Compare commits

..
Author SHA1 Message Date
Dax Raad 0023dd0813 ci: remove heavy nix-desktop workflow 2026-01-29 14:10:11 -05:00
Dax Raad 8787b527d4 ci: remove push triggers from workflow files 2026-01-29 14:06:15 -05:00
DaxandGitHub cf5cf7b23e chore: consolidate and standardize workflow files (#11183) 2026-01-29 19:04:12 +00:00
DaxandGitHub a9a7595234 test: skip failing tests (#11184) 2026-01-29 18:57:59 +00:00
9ed3b0742f ci (#11149)
Co-authored-by: opencode <opencode@sst.dev>
2026-01-29 13:17:55 -05:00
GitHub Action ae9199e101 chore: generate 2026-01-29 18:14:15 +00:00
Aiden Cline f996e05b42 chore: format code 2026-01-29 12:13:29 -06:00
Aiden Cline 8dedb3f4ae chore: regen sdk 2026-01-29 11:49:22 -06:00
45ec3105b1 feat: support config skill registration (#9640)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-01-29 11:47:06 -06:00
Frank 5a56e8172f zen: m2.1 and glm4.7 free models 2026-01-29 12:29:23 -05:00
Aiden Cline aa92ef37fd tweak: add 'skill' to permissions config section so that ides will show it as autocomplete option (this is already a respected setting) 2026-01-29 10:55:39 -06:00
d5c59a66c1 ci: added gh workflow that adds 'contributor' label to PRs/Issues (#11118)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-01-29 10:38:08 -06:00
Aiden Cline 301895c7f7 fix: ensure kimi k2.5 from fireworks ai and kimi for coding providers properly set temperature 2026-01-29 10:30:22 -06:00
Ravi KumarandGitHub 03ba49af4e fix(telemetry): restore userId and sessionId metadata in experimental_telemetry (#8195) 2026-01-29 10:14:28 -06:00
GitHub Action 008ad54cb5 ignore: update download stats 2026-01-29 2026-01-29 12:07:50 +00:00
Ryan VogelandGitHub 571751c313 fix: remove redundant Highlights heading from publish template (#11121) 2026-01-29 06:16:45 -05:00
Github Action 82717f6e8b chore: update nix node_modules hashes 2026-01-29 07:12:38 +00:00
GitHub Action c946f5f7eb chore: generate 2026-01-29 07:10:34 +00:00
2af326606c feat(desktop): Add desktop deep link (#10072)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-01-29 15:09:53 +08:00
opencode 7c0067d59d release: v1.1.42
Update Nix Hashes / update-node-modules-hashes (push) Has been cancelled
2026-01-29 05:59:07 +00:00
Aiden Cline 92eb982863 fix: undo change that used anthropic messages endpoint for anthropic models on copilot due to ratelimiting issues, go back to completions endpoint instead 2026-01-28 23:52:23 -06:00
33c5c100ff fix: frontmatter was adding newlines in some cases causing invalid model ids (#11095)
Co-authored-by: aptdnfapt <aptdnfapt@users.noreply.github.com>
2026-01-28 23:40:59 -06:00
Aiden Cline 0fabdccf11 fix: ensure that kimi doesnt have fake variants available 2026-01-28 23:40:44 -06:00
Sebastian Herrlinger 41ea4694db more timeout race guards 2026-01-29 00:17:39 -05:00
Ariane EmoryandGitHub e84d92da28 feat: Sequential numbering for forked session titles (Issue #10105) (#10321) 2026-01-28 23:08:35 -06:00
Sebastian Herrlinger 58ba486375 guard destroyed input field in timeout 2026-01-28 23:53:37 -05:00
Aiden Cline 121016af81 ci: adjust team 2026-01-28 22:28:48 -06:00
Benjamin OldenburgandGitHub f40bdd1ac3 feat(cli): include cache tokens in stats (#10582) 2026-01-28 22:03:44 -06:00
GitHub Action efc9303b27 chore: generate 2026-01-29 04:03:01 +00:00
Aiden Cline b938e1ea99 test: fix test 2026-01-28 22:02:14 -06:00
opencode 9cfdbbb1af release: v1.1.41
Update Nix Hashes / update-node-modules-hashes (push) Has been cancelled
2026-01-29 04:00:39 +00:00
Aiden Cline 29ea9fcf25 fix: ensure variants for copilot models work w/ maxTokens being set 2026-01-28 21:55:50 -06:00
idealloveandGitHub 870c38a6aa fix: maxOutputTokens was accidentally hardcoded to undefined (#10995) 2026-01-28 21:28:15 -06:00
b937fe9450 fix(provider): include providerID in SDK cache key (#11020)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 21:26:26 -06:00
tanandGitHub 427ef95f7d fix(opencode): allow media-src data: URL for small audio files (#11082) 2026-01-28 21:21:49 -06:00
GitHub Action d8fe12aafc chore: generate 2026-01-29 03:21:26 +00:00
Devin GriffinandGitHub a7d7f5bb07 fix(app): make settings more responsive for mobile and small web/desktop (#10775) 2026-01-28 21:20:34 -06:00
BabouandGitHub 8cdb82038a docs: update experimental environment variables in CLI docs (#11030) 2026-01-28 20:20:13 -06:00
Brendan Allan a9e757b5bb fix(app): types 2026-01-29 10:14:42 +08:00
Aiden Cline 4d2696e027 tweak: add ctx.abort to grep tool 2026-01-28 20:06:36 -06:00
Alex YaroshukandGitHub 0c8de47f7d fix(app): file tabs - auto scroll on open & scroll via mouse wheel (#11070) 2026-01-28 19:45:12 -06:00
Julian CoyandGitHub 7ad165fbdc fix(typescript errors): remove duplicate keys causing typescript failures (#11071) 2026-01-28 19:41:50 -06:00
Goni ZahavyandGitHub e5b33f8a5e fix(opencode): add AbortSignal support to Ripgrep.files() and GlobTool (#10833) 2026-01-28 18:47:09 -06:00
Max KongandGitHub 90a7e3d64e fix(ui): improve zh duration display formatting (#10844) 2026-01-28 16:53:37 -06:00
Max KongandGitHub 33dc70b754 fix(opencode): normalize zh punctuation for Chinese UI (#10842) 2026-01-28 16:52:04 -06:00
GitHub Action 832bbba616 chore: generate 2026-01-28 22:51:20 +00:00
Max KongandGitHub 40d5d14304 fix(app): fill missing zh keys to avoid English fallback (#10841) 2026-01-28 16:50:50 -06:00
Alex YaroshukandGitHub 36df0d823a fix(app): alignment and padding in dialogs (#10866) 2026-01-28 16:50:00 -06:00
zerone0xandGitHub 9424f829eb fix(ui): allow KaTeX inline math to be followed by punctuation (#11033) 2026-01-28 16:44:35 -06:00
GitHub Action 7b561be159 chore: generate 2026-01-28 22:35:01 +00:00
Ryan VogelandGitHub c60464de07 fix(script): remove highlights template from release notes (#11052) 2026-01-28 17:34:14 -05:00
opencode 4e41ca74b9 release: v1.1.40
Update Nix Hashes / update-node-modules-hashes (push) Has been cancelled
2026-01-28 20:09:37 +00:00
Tommy D. RossiandGitHub 8c05eb22b1 fix(markdown): Add streaming prop to markdown element (#11025) 2026-01-28 12:55:57 -05:00
adamelmore f607353be6 fix(app): close review pane 2026-01-28 10:27:01 -06:00
GitHub Action af3c97f192 chore: generate 2026-01-28 16:12:32 +00:00
26e14ce628 fix: add SubtaskPart with metadata reference (#10990)
Co-authored-by: yangtianzhe <yangtianzhe@corp.netease.com>
2026-01-28 11:11:43 -05:00
SairajandGitHub 57ad1814e3 fix(desktop): enable ctrl+n and ctrl+p for popover navigation (#10777) 2026-01-28 08:12:27 -06:00
GitHub Action 4f60ea6108 chore: generate 2026-01-28 14:11:30 +00:00
Nattawee PhantawongandGitHub 775d288027 feat(i18n): add th locale support (#10809) 2026-01-28 08:10:50 -06:00
GitHub Action bc4968abbb chore: generate 2026-01-28 13:28:52 +00:00
adamelmore acb92fcd34 chore: cleanup 2026-01-28 07:28:03 -06:00
adamelmore c9bbea4266 chore: cleanup 2026-01-28 07:28:03 -06:00
adamelmore 65e1186efe wip(app): global config 2026-01-28 07:28:03 -06:00
adamelmore 8faa2ffcf8 chore: cleanup 2026-01-28 07:28:02 -06:00
adamelmore bdfd8f8b0f feat(app): custom provider 2026-01-28 07:28:02 -06:00
adamelmore 2f35c40bb5 chore(app): global config changes 2026-01-28 07:28:02 -06:00
GitHub Action 6650aa6f35 ignore: update download stats 2026-01-28 2026-01-28 12:05:45 +00:00
Aiden Clineandopencode 8798a77a72 bump: plugins 2026-01-28 07:19:18 +00:00
opencode 6eb2bdd665 release: v1.1.39
Update Nix Hashes / update-node-modules-hashes (push) Has been cancelled
2026-01-28 07:19:17 +00:00
GitHub Action f97197bdb0 chore: generate 2026-01-28 07:05:16 +00:00
Aiden Cline 5a0b3ee673 fix: ensure copilot plugin properly sets headers for new messages api 2026-01-28 02:04:30 -05:00
opencode 3bb10773e6 release: v1.1.38
Update Nix Hashes / update-node-modules-hashes (push) Has been cancelled
2026-01-28 06:42:23 +00:00
Aiden Cline 558590712d fix: ensure parallel tool calls dont double load AGENTS.md 2026-01-28 01:38:10 -05:00
Dax Raad d76e1448f0 ci 2026-01-28 01:33:34 -05:00
Aiden Cline 026b3cc88d bump plugin version 2026-01-28 01:31:01 -05:00
Aiden Cline 28e5557bf4 ignore: adjust flag 2026-01-28 01:09:31 -05:00
GitHub Action 9a8da20a97 chore: generate 2026-01-28 06:05:21 +00:00
Goni ZahavyandGitHub 63f5669eb5 fix(opencode): ensure unsub(PartUpdated) is always called in TaskTool (#9992) 2026-01-28 01:04:43 -05:00
Frank 427cc3e153 zen: kimi k2.5 2026-01-28 00:09:33 -05:00
TitoandGitHub aedd760141 fix(cli): restore brand integrity of CLI wordmark (#10912) 2026-01-27 22:24:02 -05:00
GitHub Action 6da9fb8fb9 chore: generate 2026-01-28 03:10:48 +00:00
James MurdzaandGitHub b73e0240a5 docs: add Daytona OpenCode plugin to ecosystem (#10917) 2026-01-27 22:09:55 -05:00
GitHub Action 5f2a7c630b chore: generate 2026-01-28 03:06:58 +00:00
Tommy D. RossiandGitHub 7988f52231 feat(app): use opentui markdown component behind experimental flag (#10900) 2026-01-27 22:06:09 -05:00
143 changed files with 3811 additions and 1540 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
name: Close stale PRs
name: close-stale-prs
on:
workflow_dispatch:
+1 -1
View File
@@ -1,4 +1,4 @@
name: Daily Issues Recap
name: daily-issues-recap
on:
schedule:
+1 -1
View File
@@ -1,4 +1,4 @@
name: Daily PR Recap
name: daily-pr-recap
on:
schedule:
+1 -1
View File
@@ -1,4 +1,4 @@
name: Docs Update
name: docs-update
on:
schedule:
+1 -1
View File
@@ -1,4 +1,4 @@
name: Duplicate Issue Detection
name: duplicate-issues
on:
issues:
-46
View File
@@ -1,46 +0,0 @@
name: nix desktop
on:
push:
branches: [dev]
paths:
- "flake.nix"
- "flake.lock"
- "nix/**"
- "packages/app/**"
- "packages/desktop/**"
- ".github/workflows/nix-desktop.yml"
pull_request:
paths:
- "flake.nix"
- "flake.lock"
- "nix/**"
- "packages/app/**"
- "packages/desktop/**"
- ".github/workflows/nix-desktop.yml"
workflow_dispatch:
jobs:
build-desktop:
strategy:
fail-fast: false
matrix:
os:
- blacksmith-4vcpu-ubuntu-2404
- blacksmith-4vcpu-ubuntu-2404-arm
- macos-15-intel
- macos-latest
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Nix
uses: nixbuild/nix-quick-install-action@v34
- name: Build desktop via flake
run: |
set -euo pipefail
nix --version
nix build .#desktop -L
@@ -1,4 +1,4 @@
name: Update Nix Hashes
name: nix-hashes
permissions:
contents: write
@@ -11,17 +11,17 @@ on:
- "package.json"
- "packages/*/package.json"
- "flake.lock"
- ".github/workflows/update-nix-hashes.yml"
- ".github/workflows/nix-hashes.yml"
pull_request:
paths:
- "bun.lock"
- "package.json"
- "packages/*/package.json"
- "flake.lock"
- ".github/workflows/update-nix-hashes.yml"
- ".github/workflows/nix-hashes.yml"
jobs:
update-node-modules-hashes:
nix-hashes:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: blacksmith-4vcpu-ubuntu-2404
env:
+1 -1
View File
@@ -1,4 +1,4 @@
name: discord
name: notify-discord
on:
release:
@@ -1,4 +1,4 @@
name: Duplicate PR Check
name: pr-management
on:
pull_request_target:
@@ -63,3 +63,26 @@ jobs:
gh pr comment "$PR_NUMBER" --body "_The following comment was made by an LLM, it may be inaccurate:_
$COMMENT"
add-contributor-label:
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
steps:
- name: Add Contributor Label
uses: actions/github-script@v8
with:
script: |
const isPR = !!context.payload.pull_request;
const issueNumber = isPR ? context.payload.pull_request.number : context.payload.issue.number;
const authorAssociation = isPR ? context.payload.pull_request.author_association : context.payload.issue.author_association;
if (authorAssociation === 'CONTRIBUTOR') {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: ['contributor']
});
}
+1 -1
View File
@@ -1,4 +1,4 @@
name: PR Standards
name: pr-standards
on:
pull_request_target:
+90 -73
View File
@@ -4,6 +4,7 @@ run-name: "${{ format('release {0}', inputs.bump) }}"
on:
push:
branches:
- ci
- dev
- snapshot-*
workflow_dispatch:
@@ -29,56 +30,46 @@ permissions:
packages: write
jobs:
publish:
version:
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.repository == 'anomalyco/opencode'
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
fetch-depth: 1
- uses: ./.github/actions/setup-bun
- id: version
run: |
./script/version.ts
env:
GH_TOKEN: ${{ github.token }}
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_VERSION: ${{ inputs.version }}
outputs:
version: ${{ steps.version.outputs.version }}
release: ${{ steps.version.outputs.release }}
tag: ${{ steps.version.outputs.tag }}
- run: git fetch --force --tags
build-cli:
needs: version
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.repository == 'anomalyco/opencode'
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 1
fetch-tags: true
- uses: ./.github/actions/setup-bun
- name: Install OpenCode
if: inputs.bump || inputs.version
run: bun i -g opencode-ai@1.0.169
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Setup Git Identity
- name: Build
id: build
run: |
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
git remote set-url origin https://x-access-token:${{ secrets.SST_GITHUB_TOKEN }}@github.com/${{ github.repository }}
- name: Publish
id: publish
run: ./script/publish-start.ts
./packages/opencode/script/build.ts
env:
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_VERSION: ${{ inputs.version }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
AUR_KEY: ${{ secrets.AUR_KEY }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
NPM_CONFIG_PROVENANCE: false
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
GH_TOKEN: ${{ github.token }}
- uses: actions/upload-artifact@v4
with:
@@ -86,12 +77,12 @@ jobs:
path: packages/opencode/dist
outputs:
release: ${{ steps.publish.outputs.release }}
tag: ${{ steps.publish.outputs.tag }}
version: ${{ steps.publish.outputs.version }}
version: ${{ needs.version.outputs.version }}
publish-tauri:
needs: publish
build-tauri:
needs:
- build-cli
- version
continue-on-error: false
strategy:
fail-fast: false
@@ -111,8 +102,8 @@ jobs:
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ needs.publish.outputs.tag }}
fetch-depth: 1
fetch-tags: true
- uses: apple-actions/import-codesign-certs@v2
if: ${{ runner.os == 'macOS' }}
@@ -134,8 +125,6 @@ jobs:
run: |
echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
- run: git fetch --force --tags
- uses: ./.github/actions/setup-bun
- name: install dependencies (ubuntu only)
@@ -160,10 +149,7 @@ jobs:
bun ./scripts/prepare.ts
env:
OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
RUST_TARGET: ${{ matrix.settings.target }}
GH_TOKEN: ${{ github.token }}
GITHUB_RUN_ID: ${{ github.run_id }}
@@ -177,22 +163,18 @@ jobs:
cargo tauri --version
- name: Build and upload artifacts
uses: Wandalen/wretry.action@v3
uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
timeout-minutes: 60
with:
attempt_limit: 3
attempt_delay: 10000
action: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
with: |
projectPath: packages/desktop
uploadWorkflowArtifacts: true
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
args: --target ${{ matrix.settings.target }} --config ./src-tauri/tauri.prod.conf.json --verbose
updaterJsonPreferNsis: true
releaseId: ${{ needs.publish.outputs.release }}
tagName: ${{ needs.publish.outputs.tag }}
releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
releaseDraft: true
projectPath: packages/desktop
uploadWorkflowArtifacts: true
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
args: --target ${{ matrix.settings.target }} --config ./src-tauri/tauri.prod.conf.json --verbose
updaterJsonPreferNsis: true
releaseId: ${{ needs.version.outputs.release }}
tagName: ${{ needs.version.outputs.tag }}
releaseDraft: true
releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: true
@@ -205,20 +187,52 @@ jobs:
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
publish-release:
publish:
needs:
- publish
- publish-tauri
if: needs.publish.outputs.tag
- version
- build-cli
- build-tauri
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ needs.publish.outputs.tag }}
fetch-depth: 1
- name: Install OpenCode
if: inputs.bump || inputs.version
run: bun i -g opencode-ai
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Setup Git Identity
run: |
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
git remote set-url origin https://x-access-token:${{ secrets.SST_GITHUB_TOKEN }}@github.com/${{ github.repository }}
- uses: ./.github/actions/setup-bun
- uses: actions/download-artifact@v4
with:
name: opencode-cli
path: packages/opencode/dist
- name: Setup SSH for AUR
run: |
sudo apt-get update
@@ -230,8 +244,11 @@ jobs:
git config --global user.name "opencode"
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
- run: ./script/publish-complete.ts
- run: ./script/publish.ts
env:
OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
AUR_KEY: ${{ secrets.AUR_KEY }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
NPM_CONFIG_PROVENANCE: false
+1 -1
View File
@@ -1,4 +1,4 @@
name: Guidelines Check
name: review
on:
issue_comment:
+1 -1
View File
@@ -1,4 +1,4 @@
name: "Auto-close stale issues"
name: stale-issues
on:
schedule:
-4
View File
@@ -1,9 +1,6 @@
name: test
on:
push:
branches:
- dev
pull_request:
workflow_dispatch:
jobs:
@@ -20,7 +17,6 @@ jobs:
command: |
git config --global user.email "bot@opencode.ai"
git config --global user.name "opencode"
bun turbo typecheck
bun turbo test
- name: windows
host: windows-latest
+1 -1
View File
@@ -1,4 +1,4 @@
name: Issue Triage
name: triage
on:
issues:
+2 -1
View File
@@ -29,7 +29,8 @@
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
<a href="README.br.md">Português (Brasil)</a> |
<a href="README.th.md">ไทย</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+134
View File
@@ -0,0 +1,134 @@
<p align="center">
<a href="https://opencode.ai">
<picture>
<source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
<source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
<img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="OpenCode logo">
</picture>
</a>
</p>
<p align="center">เอเจนต์การเขียนโค้ดด้วย AI แบบโอเพนซอร์ส</p>
<p align="center">
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
<a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="สถานะการสร้าง" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
<p align="center">
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.it.md">Italiano</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a> |
<a href="README.th.md">ไทย</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
---
### การติดตั้ง
```bash
# YOLO
curl -fsSL https://opencode.ai/install | bash
# ตัวจัดการแพ็กเกจ
npm i -g opencode-ai@latest # หรือ bun/pnpm/yarn
scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS และ Linux (แนะนำ อัปเดตเสมอ)
brew install opencode # macOS และ Linux (brew formula อย่างเป็นทางการ อัปเดตน้อยกว่า)
paru -S opencode-bin # Arch Linux
mise use -g opencode # ระบบปฏิบัติการใดก็ได้
nix run nixpkgs#opencode # หรือ github:anomalyco/opencode สำหรับสาขาพัฒนาล่าสุด
```
> [!TIP]
> ลบเวอร์ชันที่เก่ากว่า 0.1.x ก่อนติดตั้ง
### แอปพลิเคชันเดสก์ท็อป (เบต้า)
OpenCode มีให้ใช้งานเป็นแอปพลิเคชันเดสก์ท็อป ดาวน์โหลดโดยตรงจาก [หน้ารุ่น](https://github.com/anomalyco/opencode/releases) หรือ [opencode.ai/download](https://opencode.ai/download)
| แพลตฟอร์ม | ดาวน์โหลด |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, หรือ AppImage |
```bash
# macOS (Homebrew)
brew install --cask opencode-desktop
# Windows (Scoop)
scoop bucket add extras; scoop install extras/opencode-desktop
```
#### ไดเรกทอรีการติดตั้ง
สคริปต์การติดตั้งจะใช้ลำดับความสำคัญตามเส้นทางการติดตั้ง:
1. `$OPENCODE_INSTALL_DIR` - ไดเรกทอรีการติดตั้งที่กำหนดเอง
2. `$XDG_BIN_DIR` - เส้นทางที่สอดคล้องกับ XDG Base Directory Specification
3. `$HOME/bin` - ไดเรกทอรีไบนารีผู้ใช้มาตรฐาน (หากมีอยู่หรือสามารถสร้างได้)
4. `$HOME/.opencode/bin` - ค่าสำรองเริ่มต้น
```bash
# ตัวอย่าง
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
```
### เอเจนต์
OpenCode รวมเอเจนต์ในตัวสองตัวที่คุณสามารถสลับได้ด้วยปุ่ม `Tab`
- **build** - เอเจนต์เริ่มต้น มีสิทธิ์เข้าถึงแบบเต็มสำหรับงานพัฒนา
- **plan** - เอเจนต์อ่านอย่างเดียวสำหรับการวิเคราะห์และการสำรวจโค้ด
- ปฏิเสธการแก้ไขไฟล์โดยค่าเริ่มต้น
- ขอสิทธิ์ก่อนเรียกใช้คำสั่ง bash
- เหมาะสำหรับสำรวจโค้ดเบสที่ไม่คุ้นเคยหรือวางแผนการเปลี่ยนแปลง
นอกจากนี้ยังมีเอเจนต์ย่อย **general** สำหรับการค้นหาที่ซับซ้อนและงานหลายขั้นตอน
ใช้ภายในและสามารถเรียกใช้ได้โดยใช้ `@general` ในข้อความ
เรียนรู้เพิ่มเติมเกี่ยวกับ [เอเจนต์](https://opencode.ai/docs/agents)
### เอกสารประกอบ
สำหรับข้อมูลเพิ่มเติมเกี่ยวกับวิธีกำหนดค่า OpenCode [**ไปที่เอกสารของเรา**](https://opencode.ai/docs)
### การมีส่วนร่วม
หากคุณสนใจที่จะมีส่วนร่วมใน OpenCode โปรดอ่าน [เอกสารการมีส่วนร่วม](./CONTRIBUTING.md) ก่อนส่ง Pull Request
### การสร้างบน OpenCode
หากคุณทำงานในโปรเจกต์ที่เกี่ยวข้องกับ OpenCode และใช้ "opencode" เป็นส่วนหนึ่งของชื่อ เช่น "opencode-dashboard" หรือ "opencode-mobile" โปรดเพิ่มหมายเหตุใน README ของคุณเพื่อชี้แจงว่าไม่ได้สร้างโดยทีม OpenCode และไม่ได้เกี่ยวข้องกับเราในทางใด
### คำถามที่พบบ่อย
#### ต่างจาก Claude Code อย่างไร?
คล้ายกับ Claude Code มากในแง่ความสามารถ นี่คือความแตกต่างหลัก:
- โอเพนซอร์ส 100%
- ไม่ผูกมัดกับผู้ให้บริการใดๆ แม้ว่าเราจะแนะนำโมเดลที่เราจัดหาให้ผ่าน [OpenCode Zen](https://opencode.ai/zen) OpenCode สามารถใช้กับ Claude, OpenAI, Google หรือแม้กระทั่งโมเดลในเครื่องได้ เมื่อโมเดลพัฒนาช่องว่างระหว่างพวกมันจะปิดลงและราคาจะลดลง ดังนั้นการไม่ผูกมัดกับผู้ให้บริการจึงสำคัญ
- รองรับ LSP ใช้งานได้ทันทีหลังการติดตั้งโดยไม่ต้องปรับแต่งหรือเปลี่ยนแปลงฟังก์ชันการทำงานใด ๆ
- เน้นที่ TUI OpenCode สร้างโดยผู้ใช้ neovim และผู้สร้าง [terminal.shop](https://terminal.shop) เราจะผลักดันขีดจำกัดของสิ่งที่เป็นไปได้ในเทอร์มินัล
- สถาปัตยกรรมไคลเอนต์/เซิร์ฟเวอร์ ตัวอย่างเช่น อาจอนุญาตให้ OpenCode ทำงานบนคอมพิวเตอร์ของคุณ ในขณะที่คุณสามารถขับเคลื่อนจากระยะไกลผ่านแอปมือถือ หมายความว่า TUI frontend เป็นหนึ่งในไคลเอนต์ที่เป็นไปได้เท่านั้น
---
**ร่วมชุมชนของเรา** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
+215 -213
View File
@@ -1,215 +1,217 @@
# Download Stats
| Date | GitHub Downloads | npm Downloads | Total |
| ---------- | -------------------- | -------------------- | -------------------- |
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |
| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) |
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |
| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) |
| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) |
| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) |
| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) |
| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) |
| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) |
| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) |
| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) |
| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) |
| 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) |
| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) |
| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) |
| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) |
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) |
| 2025-12-30 | 1,445,450 (+29,890) | 1,272,689 (+15,588) | 2,718,139 (+45,478) |
| 2025-12-31 | 1,479,598 (+34,148) | 1,293,235 (+20,546) | 2,772,833 (+54,694) |
| 2026-01-01 | 1,508,883 (+29,285) | 1,309,874 (+16,639) | 2,818,757 (+45,924) |
| 2026-01-02 | 1,563,474 (+54,591) | 1,320,959 (+11,085) | 2,884,433 (+65,676) |
| 2026-01-03 | 1,618,065 (+54,591) | 1,331,914 (+10,955) | 2,949,979 (+65,546) |
| 2026-01-04 | 1,672,656 (+39,702) | 1,339,883 (+7,969) | 3,012,539 (+62,560) |
| 2026-01-05 | 1,738,171 (+65,515) | 1,353,043 (+13,160) | 3,091,214 (+78,675) |
| 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334) | 3,338,365 (+247,151) |
| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) |
| 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) |
| 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) |
| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) |
| 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) |
| 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192) | 4,607,265 (+240,392) |
| 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391) | 4,892,140 (+284,875) |
| 2026-01-14 | 3,568,928 (+271,850) | 1,645,362 (+50,300) | 5,214,290 (+322,150) |
| 2026-01-16 | 4,121,550 (+552,622) | 1,754,418 (+109,056) | 5,875,968 (+661,678) |
| 2026-01-17 | 4,389,558 (+268,008) | 1,805,315 (+50,897) | 6,194,873 (+318,905) |
| 2026-01-18 | 4,627,623 (+238,065) | 1,839,171 (+33,856) | 6,466,794 (+271,921) |
| 2026-01-19 | 4,861,108 (+233,485) | 1,863,112 (+23,941) | 6,724,220 (+257,426) |
| 2026-01-20 | 5,128,999 (+267,891) | 1,903,665 (+40,553) | 7,032,664 (+308,444) |
| 2026-01-21 | 5,444,842 (+315,843) | 1,962,531 (+58,866) | 7,407,373 (+374,709) |
| 2026-01-22 | 5,766,340 (+321,498) | 2,029,487 (+66,956) | 7,795,827 (+388,454) |
| 2026-01-23 | 6,096,236 (+329,896) | 2,096,235 (+66,748) | 8,192,471 (+396,644) |
| 2026-01-24 | 6,371,019 (+274,783) | 2,156,870 (+60,635) | 8,527,889 (+335,418) |
| 2026-01-25 | 6,639,082 (+268,063) | 2,187,853 (+30,983) | 8,826,935 (+299,046) |
| 2026-01-26 | 6,941,620 (+302,538) | 2,232,115 (+44,262) | 9,173,735 (+346,800) |
| 2026-01-27 | 7,208,093 (+266,473) | 2,280,762 (+48,647) | 9,488,855 (+315,120) |
| Date | GitHub Downloads | npm Downloads | Total |
| ---------- | -------------------- | -------------------- | --------------------- |
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |
| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) |
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |
| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) |
| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) |
| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) |
| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) |
| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) |
| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) |
| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) |
| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) |
| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) |
| 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) |
| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) |
| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) |
| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) |
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) |
| 2025-12-30 | 1,445,450 (+29,890) | 1,272,689 (+15,588) | 2,718,139 (+45,478) |
| 2025-12-31 | 1,479,598 (+34,148) | 1,293,235 (+20,546) | 2,772,833 (+54,694) |
| 2026-01-01 | 1,508,883 (+29,285) | 1,309,874 (+16,639) | 2,818,757 (+45,924) |
| 2026-01-02 | 1,563,474 (+54,591) | 1,320,959 (+11,085) | 2,884,433 (+65,676) |
| 2026-01-03 | 1,618,065 (+54,591) | 1,331,914 (+10,955) | 2,949,979 (+65,546) |
| 2026-01-04 | 1,672,656 (+39,702) | 1,339,883 (+7,969) | 3,012,539 (+62,560) |
| 2026-01-05 | 1,738,171 (+65,515) | 1,353,043 (+13,160) | 3,091,214 (+78,675) |
| 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334) | 3,338,365 (+247,151) |
| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) |
| 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) |
| 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) |
| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) |
| 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) |
| 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192) | 4,607,265 (+240,392) |
| 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391) | 4,892,140 (+284,875) |
| 2026-01-14 | 3,568,928 (+271,850) | 1,645,362 (+50,300) | 5,214,290 (+322,150) |
| 2026-01-16 | 4,121,550 (+552,622) | 1,754,418 (+109,056) | 5,875,968 (+661,678) |
| 2026-01-17 | 4,389,558 (+268,008) | 1,805,315 (+50,897) | 6,194,873 (+318,905) |
| 2026-01-18 | 4,627,623 (+238,065) | 1,839,171 (+33,856) | 6,466,794 (+271,921) |
| 2026-01-19 | 4,861,108 (+233,485) | 1,863,112 (+23,941) | 6,724,220 (+257,426) |
| 2026-01-20 | 5,128,999 (+267,891) | 1,903,665 (+40,553) | 7,032,664 (+308,444) |
| 2026-01-21 | 5,444,842 (+315,843) | 1,962,531 (+58,866) | 7,407,373 (+374,709) |
| 2026-01-22 | 5,766,340 (+321,498) | 2,029,487 (+66,956) | 7,795,827 (+388,454) |
| 2026-01-23 | 6,096,236 (+329,896) | 2,096,235 (+66,748) | 8,192,471 (+396,644) |
| 2026-01-24 | 6,371,019 (+274,783) | 2,156,870 (+60,635) | 8,527,889 (+335,418) |
| 2026-01-25 | 6,639,082 (+268,063) | 2,187,853 (+30,983) | 8,826,935 (+299,046) |
| 2026-01-26 | 6,941,620 (+302,538) | 2,232,115 (+44,262) | 9,173,735 (+346,800) |
| 2026-01-27 | 7,208,093 (+266,473) | 2,280,762 (+48,647) | 9,488,855 (+315,120) |
| 2026-01-28 | 7,489,370 (+281,277) | 2,314,849 (+34,087) | 9,804,219 (+315,364) |
| 2026-01-29 | 7,815,471 (+326,101) | 2,374,982 (+60,133) | 10,190,453 (+386,234) |
+18 -15
View File
@@ -23,7 +23,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.1.37",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -73,7 +73,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.1.37",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -107,7 +107,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.1.37",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -134,7 +134,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.1.37",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -158,7 +158,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.1.37",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -182,13 +182,14 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.1.37",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"@solid-primitives/i18n": "2.2.1",
"@solid-primitives/storage": "catalog:",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-deep-link": "~2",
"@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-http": "~2",
"@tauri-apps/plugin-notification": "~2",
@@ -212,7 +213,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.1.37",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -241,7 +242,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.1.37",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -257,7 +258,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.1.37",
"version": "0.0.0-ci-202601291718",
"bin": {
"opencode": "./bin/opencode",
},
@@ -361,7 +362,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.1.37",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -381,7 +382,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.1.37",
"version": "0.0.0-ci-202601291718",
"devDependencies": {
"@hey-api/openapi-ts": "0.90.10",
"@tsconfig/node22": "catalog:",
@@ -392,7 +393,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.1.37",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -405,7 +406,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.1.37",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -447,7 +448,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.1.37",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"zod": "catalog:",
},
@@ -458,7 +459,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.1.37",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -1748,6 +1749,8 @@
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-EdYd4c9wGvtPB95kqtEyY+bUR+k4kRw3IA30mAQ1jPH6z57AftT8q84qwv0RDp6kkEqOBKxeInKfqi4BESYuqg=="],
"@tauri-apps/plugin-deep-link": ["@tauri-apps/plugin-deep-link@2.4.6", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-UUOSt0U5juK20uhO2MoHZX/IPblkrhUh+VPtIeu3RwtzI0R9Em3Auzfg/PwcZ9Pv8mLne3cQ4p9CFXD6WxqCZA=="],
"@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.4.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-lNIn5CZuw8WZOn8zHzmFmDSzg5zfohWoa3mdULP0YFh/VogVdMVWZPcWSHlydsiJhRQYaTNSYKN7RmZKE2lCYQ=="],
"@tauri-apps/plugin-http": ["@tauri-apps/plugin-http@2.5.4", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-/i4U/9za3mrytTgfRn5RHneKubZE/dwRmshYwyMvNRlkWjvu1m4Ma72kcbVJMZFGXpkbl+qLyWMGrihtWB76Zg=="],
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-9oI1gekRbjY6L8VwlkLdPty/9rCxC20EJlESkazEX8Y=",
"aarch64-linux": "sha256-vn+eCVanOSNfjyqHRJn4VdqbpdMoBFm49REuIkByAio=",
"aarch64-darwin": "sha256-0dMP5WbqDq3qdLRrKfmCjXz2kUDjTttGTqD3v6PDbkg=",
"x86_64-darwin": "sha256-9dEWluRXY7RTPdSEhhPsDJeGo+qa3V8dqh6n6WsLeGw="
"x86_64-linux": "sha256-yAtZlh6YR78RwPt0LK/7Pk0qUm0/97+s6ghhZzuoE/0=",
"aarch64-linux": "sha256-6j81rdjQ7Wps9bvfw+mmdwW5p01qUOwX40UZltCTe3Y=",
"aarch64-darwin": "sha256-pDM8M/QMWR6Go5pz3XXsJqcJDHAlHrx2Faijjkzcngo=",
"x86_64-darwin": "sha256-eOAPtMd1n5xYupBOevCLhY1eFy3wzGqFk/EsZocl9Y8="
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { test, expect } from "./fixtures"
test("file tree can expand folders and open a file", async ({ page, gotoSession }) => {
test.skip("file tree can expand folders and open a file", async ({ page, gotoSession }) => {
await gotoSession()
const toggle = page.getByRole("button", { name: "Toggle file tree" })
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.1.37",
"version": "0.0.0-ci-202601291718",
"description": "",
"type": "module",
"exports": {
+1 -1
View File
@@ -36,7 +36,7 @@ function writeAndWait(term: Terminal, data: string): Promise<void> {
})
}
describe("SerializeAddon", () => {
describe.skip("SerializeAddon", () => {
describe("ANSI color preservation", () => {
test("should preserve text attributes (bold, italic, underline)", async () => {
const { term, addon } = createTerminal()
+1 -1
View File
@@ -43,7 +43,7 @@ function UiI18nBridge(props: ParentProps) {
declare global {
interface Window {
__OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string }
__OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string; deepLinks?: string[] }
}
}
@@ -0,0 +1,424 @@
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { TextField } from "@opencode-ai/ui/text-field"
import { showToast } from "@opencode-ai/ui/toast"
import { For } from "solid-js"
import { createStore, produce } from "solid-js/store"
import { Link } from "@/components/link"
import { useGlobalSDK } from "@/context/global-sdk"
import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language"
import { DialogSelectProvider } from "./dialog-select-provider"
const PROVIDER_ID = /^[a-z0-9][a-z0-9-_]*$/
const OPENAI_COMPATIBLE = "@ai-sdk/openai-compatible"
type Props = {
back?: "providers" | "close"
}
export function DialogCustomProvider(props: Props) {
const dialog = useDialog()
const globalSync = useGlobalSync()
const globalSDK = useGlobalSDK()
const language = useLanguage()
const [form, setForm] = createStore({
providerID: "",
name: "",
baseURL: "",
apiKey: "",
models: [{ id: "", name: "" }],
headers: [{ key: "", value: "" }],
saving: false,
})
const [errors, setErrors] = createStore({
providerID: undefined as string | undefined,
name: undefined as string | undefined,
baseURL: undefined as string | undefined,
models: [{} as { id?: string; name?: string }],
headers: [{} as { key?: string; value?: string }],
})
const goBack = () => {
if (props.back === "close") {
dialog.close()
return
}
dialog.show(() => <DialogSelectProvider />)
}
const addModel = () => {
setForm(
"models",
produce((draft) => {
draft.push({ id: "", name: "" })
}),
)
setErrors(
"models",
produce((draft) => {
draft.push({})
}),
)
}
const removeModel = (index: number) => {
if (form.models.length <= 1) return
setForm(
"models",
produce((draft) => {
draft.splice(index, 1)
}),
)
setErrors(
"models",
produce((draft) => {
draft.splice(index, 1)
}),
)
}
const addHeader = () => {
setForm(
"headers",
produce((draft) => {
draft.push({ key: "", value: "" })
}),
)
setErrors(
"headers",
produce((draft) => {
draft.push({})
}),
)
}
const removeHeader = (index: number) => {
if (form.headers.length <= 1) return
setForm(
"headers",
produce((draft) => {
draft.splice(index, 1)
}),
)
setErrors(
"headers",
produce((draft) => {
draft.splice(index, 1)
}),
)
}
const validate = () => {
const providerID = form.providerID.trim()
const name = form.name.trim()
const baseURL = form.baseURL.trim()
const apiKey = form.apiKey.trim()
const env = apiKey.match(/^\{env:([^}]+)\}$/)?.[1]?.trim()
const key = apiKey && !env ? apiKey : undefined
const idError = !providerID
? "Provider ID is required"
: !PROVIDER_ID.test(providerID)
? "Use lowercase letters, numbers, hyphens, or underscores"
: undefined
const nameError = !name ? "Display name is required" : undefined
const urlError = !baseURL
? "Base URL is required"
: !/^https?:\/\//.test(baseURL)
? "Must start with http:// or https://"
: undefined
const disabled = (globalSync.data.config.disabled_providers ?? []).includes(providerID)
const existingProvider = globalSync.data.provider.all.find((p) => p.id === providerID)
const existsError = idError
? undefined
: existingProvider && !disabled
? "That provider ID already exists"
: undefined
const seenModels = new Set<string>()
const modelErrors = form.models.map((m) => {
const id = m.id.trim()
const modelIdError = !id
? "Required"
: seenModels.has(id)
? "Duplicate"
: (() => {
seenModels.add(id)
return undefined
})()
const modelNameError = !m.name.trim() ? "Required" : undefined
return { id: modelIdError, name: modelNameError }
})
const modelsValid = modelErrors.every((m) => !m.id && !m.name)
const models = Object.fromEntries(form.models.map((m) => [m.id.trim(), { name: m.name.trim() }]))
const seenHeaders = new Set<string>()
const headerErrors = form.headers.map((h) => {
const key = h.key.trim()
const value = h.value.trim()
if (!key && !value) return {}
const keyError = !key
? "Required"
: seenHeaders.has(key.toLowerCase())
? "Duplicate"
: (() => {
seenHeaders.add(key.toLowerCase())
return undefined
})()
const valueError = !value ? "Required" : undefined
return { key: keyError, value: valueError }
})
const headersValid = headerErrors.every((h) => !h.key && !h.value)
const headers = Object.fromEntries(
form.headers
.map((h) => ({ key: h.key.trim(), value: h.value.trim() }))
.filter((h) => !!h.key && !!h.value)
.map((h) => [h.key, h.value]),
)
setErrors(
produce((draft) => {
draft.providerID = idError ?? existsError
draft.name = nameError
draft.baseURL = urlError
draft.models = modelErrors
draft.headers = headerErrors
}),
)
const ok = !idError && !existsError && !nameError && !urlError && modelsValid && headersValid
if (!ok) return
const options = {
baseURL,
...(Object.keys(headers).length ? { headers } : {}),
}
return {
providerID,
name,
key,
config: {
npm: OPENAI_COMPATIBLE,
name,
...(env ? { env: [env] } : {}),
options,
models,
},
}
}
const save = async (e: SubmitEvent) => {
e.preventDefault()
if (form.saving) return
const result = validate()
if (!result) return
setForm("saving", true)
const disabledProviders = globalSync.data.config.disabled_providers ?? []
const nextDisabled = disabledProviders.filter((id) => id !== result.providerID)
const auth = result.key
? globalSDK.client.auth.set({
providerID: result.providerID,
auth: {
type: "api",
key: result.key,
},
})
: Promise.resolve()
auth
.then(() =>
globalSync.updateConfig({ provider: { [result.providerID]: result.config }, disabled_providers: nextDisabled }),
)
.then(() => {
dialog.close()
showToast({
variant: "success",
icon: "circle-check",
title: language.t("provider.connect.toast.connected.title", { provider: result.name }),
description: language.t("provider.connect.toast.connected.description", { provider: result.name }),
})
})
.catch((err: unknown) => {
const message = err instanceof Error ? err.message : String(err)
showToast({ title: language.t("common.requestFailed"), description: message })
})
.finally(() => {
setForm("saving", false)
})
}
return (
<Dialog
title={
<IconButton
tabIndex={-1}
icon="arrow-left"
variant="ghost"
onClick={goBack}
aria-label={language.t("common.goBack")}
/>
}
transition
>
<div class="flex flex-col gap-6 px-2.5 pb-3 overflow-y-auto max-h-[60vh]">
<div class="px-2.5 flex gap-4 items-center">
<ProviderIcon id="synthetic" class="size-5 shrink-0 icon-strong-base" />
<div class="text-16-medium text-text-strong">Custom provider</div>
</div>
<form onSubmit={save} class="px-2.5 pb-6 flex flex-col gap-6">
<p class="text-14-regular text-text-base">
Configure an OpenAI-compatible provider. See the{" "}
<Link href="https://opencode.ai/docs/providers/#custom-provider" tabIndex={-1}>
provider config docs
</Link>
.
</p>
<div class="flex flex-col gap-4">
<TextField
autofocus
label="Provider ID"
placeholder="myprovider"
description="Lowercase letters, numbers, hyphens, or underscores"
value={form.providerID}
onChange={setForm.bind(null, "providerID")}
validationState={errors.providerID ? "invalid" : undefined}
error={errors.providerID}
/>
<TextField
label="Display name"
placeholder="My AI Provider"
value={form.name}
onChange={setForm.bind(null, "name")}
validationState={errors.name ? "invalid" : undefined}
error={errors.name}
/>
<TextField
label="Base URL"
placeholder="https://api.myprovider.com/v1"
value={form.baseURL}
onChange={setForm.bind(null, "baseURL")}
validationState={errors.baseURL ? "invalid" : undefined}
error={errors.baseURL}
/>
<TextField
label="API key"
placeholder="API key"
description="Optional. Leave empty if you manage auth via headers."
value={form.apiKey}
onChange={setForm.bind(null, "apiKey")}
/>
</div>
<div class="flex flex-col gap-3">
<label class="text-12-medium text-text-weak">Models</label>
<For each={form.models}>
{(m, i) => (
<div class="flex gap-2 items-start">
<div class="flex-1">
<TextField
label="ID"
hideLabel
placeholder="model-id"
value={m.id}
onChange={(v) => setForm("models", i(), "id", v)}
validationState={errors.models[i()]?.id ? "invalid" : undefined}
error={errors.models[i()]?.id}
/>
</div>
<div class="flex-1">
<TextField
label="Name"
hideLabel
placeholder="Display Name"
value={m.name}
onChange={(v) => setForm("models", i(), "name", v)}
validationState={errors.models[i()]?.name ? "invalid" : undefined}
error={errors.models[i()]?.name}
/>
</div>
<IconButton
type="button"
icon="trash"
variant="ghost"
class="mt-1.5"
onClick={() => removeModel(i())}
disabled={form.models.length <= 1}
aria-label="Remove model"
/>
</div>
)}
</For>
<Button type="button" size="small" variant="ghost" icon="plus-small" onClick={addModel} class="self-start">
Add model
</Button>
</div>
<div class="flex flex-col gap-3">
<label class="text-12-medium text-text-weak">Headers (optional)</label>
<For each={form.headers}>
{(h, i) => (
<div class="flex gap-2 items-start">
<div class="flex-1">
<TextField
label="Header"
hideLabel
placeholder="Header-Name"
value={h.key}
onChange={(v) => setForm("headers", i(), "key", v)}
validationState={errors.headers[i()]?.key ? "invalid" : undefined}
error={errors.headers[i()]?.key}
/>
</div>
<div class="flex-1">
<TextField
label="Value"
hideLabel
placeholder="value"
value={h.value}
onChange={(v) => setForm("headers", i(), "value", v)}
validationState={errors.headers[i()]?.value ? "invalid" : undefined}
error={errors.headers[i()]?.value}
/>
</div>
<IconButton
type="button"
icon="trash"
variant="ghost"
class="mt-1.5"
onClick={() => removeHeader(i())}
disabled={form.headers.length <= 1}
aria-label="Remove header"
/>
</div>
)}
</For>
<Button type="button" size="small" variant="ghost" icon="plus-small" onClick={addHeader} class="self-start">
Add header
</Button>
</div>
<Button class="w-auto self-start" type="submit" size="large" variant="primary" disabled={form.saving}>
{form.saving ? "Saving..." : language.t("common.submit")}
</Button>
</form>
</div>
</Dialog>
)
}
@@ -44,7 +44,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
"session.previous",
"session.next",
"terminal.toggle",
"fileTree.toggle",
"review.toggle",
]
const limit = 5
@@ -162,6 +162,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
const value = file.tab(path)
tabs().open(value)
file.load(path)
layout.fileTree.open()
layout.fileTree.setTab("all")
props.onOpenFile?.(path)
}
@@ -195,7 +196,6 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
: language.t("palette.search.placeholder"),
autofocus: true,
hideIcon: true,
class: "pl-3 pr-2 !mb-0",
}}
emptyMessage={language.t("palette.empty")}
loadingMessage={language.t("common.loading")}
@@ -223,7 +223,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
</div>
}
>
<div class="w-full flex items-center justify-between gap-4 pl-1">
<div class="w-full flex items-center justify-between gap-4">
<div class="flex items-center gap-2 min-w-0">
<span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
<Show when={item.description}>
@@ -187,7 +187,7 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
<Kobalte.Portal>
<Kobalte.Content
ref={(el) => setStore("content", el)}
class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden"
class="w-72 h-80 flex flex-col p-2 rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden"
onEscapeKeyDown={(event) => {
setStore("dismiss", "escape")
setStore("open", false)
@@ -5,9 +5,17 @@ import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
import { Tag } from "@opencode-ai/ui/tag"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { IconName } from "@opencode-ai/ui/icons/provider"
import { iconNames, type IconName } from "@opencode-ai/ui/icons/provider"
import { DialogConnectProvider } from "./dialog-connect-provider"
import { useLanguage } from "@/context/language"
import { DialogCustomProvider } from "./dialog-custom-provider"
const CUSTOM_ID = "_custom"
function icon(id: string): IconName {
if (iconNames.includes(id as IconName)) return id as IconName
return "synthetic"
}
export const DialogSelectProvider: Component = () => {
const dialog = useDialog()
@@ -26,11 +34,13 @@ export const DialogSelectProvider: Component = () => {
key={(x) => x?.id}
items={() => {
language.locale()
return providers.all()
return [{ id: CUSTOM_ID, name: "Custom provider" }, ...providers.all()]
}}
filterKeys={["id", "name"]}
groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())}
sortBy={(a, b) => {
if (a.id === CUSTOM_ID) return -1
if (b.id === CUSTOM_ID) return 1
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
return a.name.localeCompare(b.name)
@@ -43,13 +53,20 @@ export const DialogSelectProvider: Component = () => {
}}
onSelect={(x) => {
if (!x) return
if (x.id === CUSTOM_ID) {
dialog.show(() => <DialogCustomProvider back="providers" />)
return
}
dialog.show(() => <DialogConnectProvider provider={x.id} />)
}}
>
{(i) => (
<div class="px-1.25 w-full flex items-center gap-x-3">
<ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
<ProviderIcon data-slot="list-item-extra-icon" id={icon(i.id)} />
<span>{i.name}</span>
<Show when={i.id === CUSTOM_ID}>
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
</Show>
<Show when={i.id === "opencode"}>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Show>
+7 -4
View File
@@ -189,12 +189,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const wantsReview = item.commentOrigin === "review" || (item.commentOrigin !== "file" && commentInReview(item.path))
if (wantsReview) {
layout.fileTree.open()
layout.fileTree.setTab("changes")
if (!layout.fileTree.opened()) tabs().open("review")
requestAnimationFrame(() => comments.setFocus(focus))
return
}
layout.fileTree.open()
layout.fileTree.setTab("all")
const tab = files.tab(item.path)
tabs().open(tab)
@@ -1037,13 +1038,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return
}
const ctrl = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey
if (store.popover) {
if (event.key === "Tab") {
selectPopoverActive()
event.preventDefault()
return
}
if (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter") {
const nav = event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter"
const ctrlNav = ctrl && (event.key === "n" || event.key === "p")
if (nav || ctrlNav) {
if (store.popover === "at") {
atOnKeyDown(event)
event.preventDefault()
@@ -1057,8 +1062,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
}
const ctrl = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey
if (ctrl && event.code === "KeyG") {
if (store.popover) {
setStore("popover", null)
@@ -57,6 +57,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
const openContext = () => {
if (!params.id) return
layout.fileTree.open()
layout.fileTree.setTab("all")
tabs().open("context")
tabs().setActive("context")
@@ -280,17 +280,14 @@ export function SessionHeader() {
</TooltipKeybind>
</div>
<div class="hidden md:block shrink-0">
<TooltipKeybind
title={language.t("command.fileTree.toggle")}
keybind={command.keybind("fileTree.toggle")}
>
<TooltipKeybind title={language.t("command.review.toggle")} keybind={command.keybind("review.toggle")}>
<Button
variant="ghost"
class="group/file-tree-toggle size-6 p-0"
onClick={() => layout.fileTree.toggle()}
aria-label={language.t("command.fileTree.toggle")}
aria-label={language.t("command.review.toggle")}
aria-expanded={layout.fileTree.opened()}
aria-controls="file-tree-panel"
aria-controls="review-panel"
>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
@@ -130,7 +130,7 @@ export const SettingsGeneral: Component = () => {
const soundOptions = [...SOUND_OPTIONS]
return (
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-10 pb-10">
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
<div class="flex flex-col gap-1 pt-6 pb-8">
<h2 class="text-16-medium text-text-strong">{language.t("settings.tab.general")}</h2>
@@ -406,8 +406,8 @@ interface SettingsRowProps {
const SettingsRow: Component<SettingsRowProps> = (props) => {
return (
<div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
<div class="flex flex-col gap-0.5">
<div class="flex flex-wrap items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
<div class="flex flex-col gap-0.5 min-w-0">
<span class="text-14-medium text-text-strong">{props.title}</span>
<span class="text-12-regular text-text-weak">{props.description}</span>
</div>
@@ -352,7 +352,7 @@ export const SettingsKeybinds: Component = () => {
})
return (
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-10 pb-10">
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
<div class="flex items-center justify-between gap-4">
@@ -39,7 +39,7 @@ export const SettingsModels: Component = () => {
})
return (
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-10 pb-10">
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
<h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
@@ -99,7 +99,7 @@ export const SettingsModels: Component = () => {
{(item) => {
const key = { providerID: item.provider.id, modelID: item.id }
return (
<div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
<div class="flex flex-wrap items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
<div class="min-w-0">
<span class="text-14-regular text-text-strong truncate block">{item.name}</span>
</div>
@@ -176,13 +176,13 @@ export const SettingsPermissions: Component = () => {
return (
<div class="flex flex-col h-full overflow-y-auto no-scrollbar">
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
<div class="flex flex-col gap-1 p-8 max-w-[720px]">
<div class="flex flex-col gap-1 px-4 py-8 sm:p-8 max-w-[720px]">
<h2 class="text-16-medium text-text-strong">{language.t("settings.permissions.title")}</h2>
<p class="text-14-regular text-text-weak">{language.t("settings.permissions.description")}</p>
</div>
</div>
<div class="flex flex-col gap-6 p-8 pt-6 max-w-[720px]">
<div class="flex flex-col gap-6 px-4 py-6 sm:p-8 sm:pt-6 max-w-[720px]">
<div class="flex flex-col gap-2">
<h3 class="text-14-medium text-text-strong">{language.t("settings.permissions.section.tools")}</h3>
<div class="border border-border-weak-base rounded-lg overflow-hidden">
@@ -217,8 +217,8 @@ interface SettingsRowProps {
const SettingsRow: Component<SettingsRowProps> = (props) => {
return (
<div class="flex items-center justify-between gap-4 px-4 py-3 border-b border-border-weak-base last:border-none">
<div class="flex flex-col gap-0.5">
<div class="flex flex-wrap items-center justify-between gap-4 px-4 py-3 border-b border-border-weak-base last:border-none">
<div class="flex flex-col gap-0.5 min-w-0">
<span class="text-14-medium text-text-strong">{props.title}</span>
<span class="text-12-regular text-text-weak">{props.description}</span>
</div>
@@ -3,13 +3,15 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Tag } from "@opencode-ai/ui/tag"
import { showToast } from "@opencode-ai/ui/toast"
import type { IconName } from "@opencode-ai/ui/icons/provider"
import { iconNames, type IconName } from "@opencode-ai/ui/icons/provider"
import { popularProviders, useProviders } from "@/hooks/use-providers"
import { createMemo, type Component, For, Show } from "solid-js"
import { useLanguage } from "@/context/language"
import { useGlobalSDK } from "@/context/global-sdk"
import { useGlobalSync } from "@/context/global-sync"
import { DialogConnectProvider } from "./dialog-connect-provider"
import { DialogSelectProvider } from "./dialog-select-provider"
import { DialogCustomProvider } from "./dialog-custom-provider"
type ProviderSource = "env" | "api" | "config" | "custom"
type ProviderMeta = { source?: ProviderSource }
@@ -18,8 +20,14 @@ export const SettingsProviders: Component = () => {
const dialog = useDialog()
const language = useLanguage()
const globalSDK = useGlobalSDK()
const globalSync = useGlobalSync()
const providers = useProviders()
const icon = (id: string): IconName => {
if (iconNames.includes(id as IconName)) return id as IconName
return "synthetic"
}
const connected = createMemo(() => {
return providers
.connected()
@@ -42,14 +50,53 @@ export const SettingsProviders: Component = () => {
const current = source(item)
if (current === "env") return language.t("settings.providers.tag.environment")
if (current === "api") return language.t("provider.connect.method.apiKey")
if (current === "config") return language.t("settings.providers.tag.config")
if (current === "config") {
const id = (item as { id?: string }).id
if (id && isConfigCustom(id)) return language.t("settings.providers.tag.custom")
return language.t("settings.providers.tag.config")
}
if (current === "custom") return language.t("settings.providers.tag.custom")
return language.t("settings.providers.tag.other")
}
const canDisconnect = (item: unknown) => source(item) !== "env"
const isConfigCustom = (providerID: string) => {
const provider = globalSync.data.config.provider?.[providerID]
if (!provider) return false
if (provider.npm !== "@ai-sdk/openai-compatible") return false
if (!provider.models || Object.keys(provider.models).length === 0) return false
return true
}
const disableProvider = async (providerID: string, name: string) => {
const before = globalSync.data.config.disabled_providers ?? []
const next = before.includes(providerID) ? before : [...before, providerID]
globalSync.set("config", "disabled_providers", next)
await globalSync
.updateConfig({ disabled_providers: next })
.then(() => {
showToast({
variant: "success",
icon: "circle-check",
title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }),
description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }),
})
})
.catch((err: unknown) => {
globalSync.set("config", "disabled_providers", before)
const message = err instanceof Error ? err.message : String(err)
showToast({ title: language.t("common.requestFailed"), description: message })
})
}
const disconnect = async (providerID: string, name: string) => {
if (isConfigCustom(providerID)) {
await globalSDK.client.auth.remove({ providerID }).catch(() => undefined)
await disableProvider(providerID, name)
return
}
await globalSDK.client.auth
.remove({ providerID })
.then(async () => {
@@ -68,7 +115,7 @@ export const SettingsProviders: Component = () => {
}
return (
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-10 pb-10">
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
<div class="flex flex-col gap-1 pt-6 pb-8 max-w-[720px]">
<h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
@@ -89,9 +136,9 @@ export const SettingsProviders: Component = () => {
>
<For each={connected()}>
{(item) => (
<div class="group flex items-center justify-between gap-4 h-16 border-b border-border-weak-base last:border-none">
<div class="group flex flex-wrap items-center justify-between gap-4 min-h-16 py-3 border-b border-border-weak-base last:border-none">
<div class="flex items-center gap-3 min-w-0">
<ProviderIcon id={item.id as IconName} class="size-5 shrink-0 icon-strong-base" />
<ProviderIcon id={icon(item.id)} class="size-5 shrink-0 icon-strong-base" />
<span class="text-14-medium text-text-strong truncate">{item.name}</span>
<Tag>{type(item)}</Tag>
</div>
@@ -119,10 +166,10 @@ export const SettingsProviders: Component = () => {
<div class="bg-surface-raised-base px-4 rounded-lg">
<For each={popular()}>
{(item) => (
<div class="flex items-center justify-between gap-4 h-16 border-b border-border-weak-base last:border-none">
<div class="flex flex-wrap items-center justify-between gap-4 min-h-16 py-3 border-b border-border-weak-base last:border-none">
<div class="flex flex-col min-w-0">
<div class="flex items-center gap-x-3">
<ProviderIcon id={item.id as IconName} class="size-5 shrink-0 icon-strong-base" />
<ProviderIcon id={icon(item.id)} class="size-5 shrink-0 icon-strong-base" />
<span class="text-14-medium text-text-strong">{item.name}</span>
<Show when={item.id === "opencode"}>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
@@ -177,6 +224,27 @@ export const SettingsProviders: Component = () => {
</div>
)}
</For>
<div class="flex items-center justify-between gap-4 h-16 border-b border-border-weak-base last:border-none">
<div class="flex flex-col min-w-0">
<div class="flex items-center gap-x-3">
<ProviderIcon id={icon("synthetic")} class="size-5 shrink-0 icon-strong-base" />
<span class="text-14-medium text-text-strong">Custom provider</span>
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
</div>
<span class="text-12-regular text-text-weak pl-8">Add an OpenAI-compatible provider by base URL.</span>
</div>
<Button
size="large"
variant="secondary"
icon="plus-small"
onClick={() => {
dialog.show(() => <DialogCustomProvider back="close" />)
}}
>
{language.t("common.connect")}
</Button>
</div>
</div>
<Button
+84 -24
View File
@@ -188,7 +188,74 @@ function createGlobalSync() {
config: {},
reload: undefined,
})
let bootstrapQueue: string[] = []
const queued = new Set<string>()
let root = false
let running = false
let timer: ReturnType<typeof setTimeout> | undefined
const paused = () => untrack(() => globalStore.reload) !== undefined
const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0))
const take = (count: number) => {
if (queued.size === 0) return [] as string[]
const items: string[] = []
for (const item of queued) {
queued.delete(item)
items.push(item)
if (items.length >= count) break
}
return items
}
const schedule = () => {
if (timer) return
timer = setTimeout(() => {
timer = undefined
void drain()
}, 0)
}
const push = (directory: string) => {
if (!directory) return
queued.add(directory)
if (paused()) return
schedule()
}
const refresh = () => {
root = true
if (paused()) return
schedule()
}
async function drain() {
if (running) return
running = true
try {
while (true) {
if (paused()) return
if (root) {
root = false
await bootstrap()
await tick()
continue
}
const dirs = take(2)
if (dirs.length === 0) return
await Promise.all(dirs.map((dir) => bootstrapInstance(dir)))
await tick()
}
} finally {
running = false
if (paused()) return
if (root || queued.size) schedule()
}
}
createEffect(() => {
if (!projectCacheReady()) return
@@ -210,14 +277,8 @@ function createGlobalSync() {
createEffect(() => {
if (globalStore.reload !== "complete") return
if (bootstrapQueue.length) {
for (const directory of bootstrapQueue) {
bootstrapInstance(directory)
}
bootstrap()
}
bootstrapQueue = []
setGlobalStore("reload", undefined)
refresh()
})
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
@@ -584,9 +645,8 @@ function createGlobalSync() {
if (directory === "global") {
switch (event?.type) {
case "global.disposed": {
if (globalStore.reload) return
bootstrap()
break
refresh()
return
}
case "project.updated": {
const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id)
@@ -647,12 +707,8 @@ function createGlobalSync() {
switch (event.type) {
case "server.instance.disposed": {
if (globalStore.reload) {
bootstrapQueue.push(directory)
return
}
bootstrapInstance(directory)
break
push(directory)
return
}
case "session.created": {
const info = event.properties.info
@@ -893,6 +949,10 @@ function createGlobalSync() {
}
})
onCleanup(unsub)
onCleanup(() => {
if (!timer) return
clearTimeout(timer)
})
async function bootstrap() {
const health = await globalSDK.client.global
@@ -916,7 +976,7 @@ function createGlobalSync() {
}),
),
retry(() =>
globalSDK.client.config.get().then((x) => {
globalSDK.client.global.config.get().then((x) => {
setGlobalStore("config", x.data!)
}),
),
@@ -999,13 +1059,13 @@ function createGlobalSync() {
},
child,
bootstrap,
updateConfig: async (config: Config) => {
updateConfig: (config: Config) => {
setGlobalStore("reload", "pending")
const response = await globalSDK.client.config.update({ config })
setTimeout(() => {
setGlobalStore("reload", "complete")
}, 1000)
return response
return globalSDK.client.global.config.update({ config }).finally(() => {
setTimeout(() => {
setGlobalStore("reload", "complete")
}, 1000)
})
},
project: {
loadSessions,
+39 -2
View File
@@ -17,6 +17,7 @@ import { dict as ru } from "@/i18n/ru"
import { dict as ar } from "@/i18n/ar"
import { dict as no } from "@/i18n/no"
import { dict as br } from "@/i18n/br"
import { dict as th } from "@/i18n/th"
import { dict as uiEn } from "@opencode-ai/ui/i18n/en"
import { dict as uiZh } from "@opencode-ai/ui/i18n/zh"
import { dict as uiZht } from "@opencode-ai/ui/i18n/zht"
@@ -31,13 +32,45 @@ import { dict as uiRu } from "@opencode-ai/ui/i18n/ru"
import { dict as uiAr } from "@opencode-ai/ui/i18n/ar"
import { dict as uiNo } from "@opencode-ai/ui/i18n/no"
import { dict as uiBr } from "@opencode-ai/ui/i18n/br"
import { dict as uiTh } from "@opencode-ai/ui/i18n/th"
export type Locale = "en" | "zh" | "zht" | "ko" | "de" | "es" | "fr" | "da" | "ja" | "pl" | "ru" | "ar" | "no" | "br"
export type Locale =
| "en"
| "zh"
| "zht"
| "ko"
| "de"
| "es"
| "fr"
| "da"
| "ja"
| "pl"
| "ru"
| "ar"
| "no"
| "br"
| "th"
type RawDictionary = typeof en & typeof uiEn
type Dictionary = i18n.Flatten<RawDictionary>
const LOCALES: readonly Locale[] = ["en", "zh", "zht", "ko", "de", "es", "fr", "da", "ja", "pl", "ru", "ar", "no", "br"]
const LOCALES: readonly Locale[] = [
"en",
"zh",
"zht",
"ko",
"de",
"es",
"fr",
"da",
"ja",
"pl",
"ru",
"ar",
"no",
"br",
"th",
]
function detectLocale(): Locale {
if (typeof navigator !== "object") return "en"
@@ -65,6 +98,7 @@ function detectLocale(): Locale {
)
return "no"
if (language.toLowerCase().startsWith("pt")) return "br"
if (language.toLowerCase().startsWith("th")) return "th"
}
return "en"
@@ -94,6 +128,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
if (store.locale === "ar") return "ar"
if (store.locale === "no") return "no"
if (store.locale === "br") return "br"
if (store.locale === "th") return "th"
return "en"
})
@@ -118,6 +153,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
if (locale() === "ar") return { ...base, ...i18n.flatten({ ...ar, ...uiAr }) }
if (locale() === "no") return { ...base, ...i18n.flatten({ ...no, ...uiNo }) }
if (locale() === "br") return { ...base, ...i18n.flatten({ ...br, ...uiBr }) }
if (locale() === "th") return { ...base, ...i18n.flatten({ ...th, ...uiTh }) }
return { ...base, ...i18n.flatten({ ...ko, ...uiKo }) }
})
@@ -138,6 +174,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
ar: "language.ar",
no: "language.no",
br: "language.br",
th: "language.th",
}
const label = (value: Locale) => t(labelKey[value])
@@ -5,7 +5,7 @@ import { makePersisted, type SyncStorage } from "@solid-primitives/storage"
import { createScrollPersistence } from "./layout-scroll"
describe("createScrollPersistence", () => {
test("debounces persisted scroll writes", async () => {
test.skip("debounces persisted scroll writes", async () => {
const key = "layout-scroll.test"
const data = new Map<string, string>()
const writes: string[] = []
+3 -10
View File
@@ -624,10 +624,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] })
return {
tabs,
active: createMemo(() => tabs().active),
active: createMemo(() => (tabs().active === "review" ? undefined : tabs().active)),
all: createMemo(() => tabs().all.filter((tab) => tab !== "review")),
setActive(tab: string | undefined) {
const session = key()
if (tab === "review") return
if (!store.sessionTabs[session]) {
setStore("sessionTabs", session, { all: [], active: tab })
} else {
@@ -644,18 +645,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}
},
async open(tab: string) {
if (tab === "review") return
const session = key()
const current = store.sessionTabs[session] ?? { all: [] }
if (tab === "review") {
if (!store.sessionTabs[session]) {
setStore("sessionTabs", session, { all: current.all, active: tab })
return
}
setStore("sessionTabs", session, "active", tab)
return
}
if (tab === "context") {
const all = [tab, ...current.all.filter((x) => x !== tab)]
if (!store.sessionTabs[session]) {
+1
View File
@@ -331,6 +331,7 @@ export const dict = {
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "لغة",
"toast.language.description": "تم التبديل إلى {{language}}",
+1
View File
@@ -330,6 +330,7 @@ export const dict = {
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Idioma",
"toast.language.description": "Alterado para {{language}}",
+1
View File
@@ -332,6 +332,7 @@ export const dict = {
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Sprog",
"toast.language.description": "Skiftede til {{language}}",
+1
View File
@@ -338,6 +338,7 @@ export const dict = {
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Sprache",
"toast.language.description": "Zu {{language}} gewechselt",
+1
View File
@@ -337,6 +337,7 @@ export const dict = {
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Language",
"toast.language.description": "Switched to {{language}}",
+1
View File
@@ -333,6 +333,7 @@ export const dict = {
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Idioma",
"toast.language.description": "Cambiado a {{language}}",
+1
View File
@@ -333,6 +333,7 @@ export const dict = {
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Langue",
"toast.language.description": "Passé à {{language}}",
+1
View File
@@ -331,6 +331,7 @@ export const dict = {
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "言語",
"toast.language.description": "{{language}}に切り替えました",
+1
View File
@@ -334,6 +334,7 @@ export const dict = {
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "언어",
"toast.language.description": "{{language}}(으)로 전환됨",
+1
View File
@@ -334,6 +334,7 @@ export const dict = {
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Språk",
"toast.language.description": "Byttet til {{language}}",
+1
View File
@@ -332,6 +332,7 @@ export const dict = {
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Język",
"toast.language.description": "Przełączono na {{language}}",
+1
View File
@@ -333,6 +333,7 @@ export const dict = {
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Язык",
"toast.language.description": "Переключено на {{language}}",
+718
View File
@@ -0,0 +1,718 @@
export const dict = {
"command.category.suggested": "แนะนำ",
"command.category.view": "มุมมอง",
"command.category.project": "โปรเจกต์",
"command.category.provider": "ผู้ให้บริการ",
"command.category.server": "เซิร์ฟเวอร์",
"command.category.session": "เซสชัน",
"command.category.theme": "ธีม",
"command.category.language": "ภาษา",
"command.category.file": "ไฟล์",
"command.category.context": "บริบท",
"command.category.terminal": "เทอร์มินัล",
"command.category.model": "โมเดล",
"command.category.mcp": "MCP",
"command.category.agent": "เอเจนต์",
"command.category.permissions": "สิทธิ์",
"command.category.workspace": "พื้นที่ทำงาน",
"command.category.settings": "การตั้งค่า",
"theme.scheme.system": "ระบบ",
"theme.scheme.light": "สว่าง",
"theme.scheme.dark": "มืด",
"command.sidebar.toggle": "สลับแถบข้าง",
"command.project.open": "เปิดโปรเจกต์",
"command.provider.connect": "เชื่อมต่อผู้ให้บริการ",
"command.server.switch": "สลับเซิร์ฟเวอร์",
"command.settings.open": "เปิดการตั้งค่า",
"command.session.previous": "เซสชันก่อนหน้า",
"command.session.next": "เซสชันถัดไป",
"command.session.archive": "จัดเก็บเซสชัน",
"command.palette": "คำสั่งค้นหา",
"command.theme.cycle": "เปลี่ยนธีม",
"command.theme.set": "ใช้ธีม: {{theme}}",
"command.theme.scheme.cycle": "เปลี่ยนโทนสี",
"command.theme.scheme.set": "ใช้โทนสี: {{scheme}}",
"command.language.cycle": "เปลี่ยนภาษา",
"command.language.set": "ใช้ภาษา: {{language}}",
"command.session.new": "เซสชันใหม่",
"command.file.open": "เปิดไฟล์",
"command.file.open.description": "ค้นหาไฟล์และคำสั่ง",
"command.context.addSelection": "เพิ่มส่วนที่เลือกไปยังบริบท",
"command.context.addSelection.description": "เพิ่มบรรทัดที่เลือกจากไฟล์ปัจจุบัน",
"command.terminal.toggle": "สลับเทอร์มินัล",
"command.fileTree.toggle": "สลับต้นไม้ไฟล์",
"command.review.toggle": "สลับการตรวจสอบ",
"command.terminal.new": "เทอร์มินัลใหม่",
"command.terminal.new.description": "สร้างแท็บเทอร์มินัลใหม่",
"command.steps.toggle": "สลับขั้นตอน",
"command.steps.toggle.description": "แสดงหรือซ่อนขั้นตอนสำหรับข้อความปัจจุบัน",
"command.message.previous": "ข้อความก่อนหน้า",
"command.message.previous.description": "ไปที่ข้อความผู้ใช้ก่อนหน้า",
"command.message.next": "ข้อความถัดไป",
"command.message.next.description": "ไปที่ข้อความผู้ใช้ถัดไป",
"command.model.choose": "เลือกโมเดล",
"command.model.choose.description": "เลือกโมเดลอื่น",
"command.mcp.toggle": "สลับ MCPs",
"command.mcp.toggle.description": "สลับ MCPs",
"command.agent.cycle": "เปลี่ยนเอเจนต์",
"command.agent.cycle.description": "สลับไปยังเอเจนต์ถัดไป",
"command.agent.cycle.reverse": "เปลี่ยนเอเจนต์ย้อนกลับ",
"command.agent.cycle.reverse.description": "สลับไปยังเอเจนต์ก่อนหน้า",
"command.model.variant.cycle": "เปลี่ยนความพยายามในการคิด",
"command.model.variant.cycle.description": "สลับไปยังระดับความพยายามถัดไป",
"command.permissions.autoaccept.enable": "ยอมรับการแก้ไขโดยอัตโนมัติ",
"command.permissions.autoaccept.disable": "หยุดยอมรับการแก้ไขโดยอัตโนมัติ",
"command.session.undo": "ยกเลิก",
"command.session.undo.description": "ยกเลิกข้อความล่าสุด",
"command.session.redo": "ทำซ้ำ",
"command.session.redo.description": "ทำซ้ำข้อความที่ถูกยกเลิกล่าสุด",
"command.session.compact": "บีบอัดเซสชัน",
"command.session.compact.description": "สรุปเซสชันเพื่อลดขนาดบริบท",
"command.session.fork": "แตกแขนงจากข้อความ",
"command.session.fork.description": "สร้างเซสชันใหม่จากข้อความก่อนหน้า",
"command.session.share": "แชร์เซสชัน",
"command.session.share.description": "แชร์เซสชันนี้และคัดลอก URL ไปยังคลิปบอร์ด",
"command.session.unshare": "ยกเลิกการแชร์เซสชัน",
"command.session.unshare.description": "หยุดการแชร์เซสชันนี้",
"palette.search.placeholder": "ค้นหาไฟล์และคำสั่ง",
"palette.empty": "ไม่พบผลลัพธ์",
"palette.group.commands": "คำสั่ง",
"palette.group.files": "ไฟล์",
"dialog.provider.search.placeholder": "ค้นหาผู้ให้บริการ",
"dialog.provider.empty": "ไม่พบผู้ให้บริการ",
"dialog.provider.group.popular": "ยอดนิยม",
"dialog.provider.group.other": "อื่น ๆ",
"dialog.provider.tag.recommended": "แนะนำ",
"dialog.provider.opencode.note": "โมเดลที่คัดสรร รวมถึง Claude, GPT, Gemini และอื่น ๆ",
"dialog.provider.anthropic.note": "เข้าถึงโมเดล Claude โดยตรง รวมถึง Pro และ Max",
"dialog.provider.copilot.note": "โมเดล Claude สำหรับการช่วยเหลือในการเขียนโค้ด",
"dialog.provider.openai.note": "โมเดล GPT สำหรับงาน AI ทั่วไปที่รวดเร็วและมีความสามารถ",
"dialog.provider.google.note": "โมเดล Gemini สำหรับการตอบสนองที่รวดเร็วและมีโครงสร้าง",
"dialog.provider.openrouter.note": "เข้าถึงโมเดลที่รองรับทั้งหมดจากผู้ให้บริการเดียว",
"dialog.provider.vercel.note": "การเข้าถึงโมเดล AI แบบรวมด้วยการกำหนดเส้นทางอัจฉริยะ",
"dialog.model.select.title": "เลือกโมเดล",
"dialog.model.search.placeholder": "ค้นหาโมเดล",
"dialog.model.empty": "ไม่พบผลลัพธ์โมเดล",
"dialog.model.manage": "จัดการโมเดล",
"dialog.model.manage.description": "ปรับแต่งโมเดลที่จะปรากฏในตัวเลือกโมเดล",
"dialog.model.unpaid.freeModels.title": "โมเดลฟรีที่จัดหาให้โดย OpenCode",
"dialog.model.unpaid.addMore.title": "เพิ่มโมเดลเพิ่มเติมจากผู้ให้บริการยอดนิยม",
"dialog.provider.viewAll": "แสดงผู้ให้บริการเพิ่มเติม",
"provider.connect.title": "เชื่อมต่อ {{provider}}",
"provider.connect.title.anthropicProMax": "เข้าสู่ระบบด้วย Claude Pro/Max",
"provider.connect.selectMethod": "เลือกวิธีการเข้าสู่ระบบสำหรับ {{provider}}",
"provider.connect.method.apiKey": "คีย์ API",
"provider.connect.status.inProgress": "กำลังอนุญาต...",
"provider.connect.status.waiting": "รอการอนุญาต...",
"provider.connect.status.failed": "การอนุญาตล้มเหลว: {{error}}",
"provider.connect.apiKey.description":
"ป้อนคีย์ API ของ {{provider}} เพื่อเชื่อมต่อบัญชีและใช้โมเดล {{provider}} ใน OpenCode",
"provider.connect.apiKey.label": "คีย์ API ของ {{provider}}",
"provider.connect.apiKey.placeholder": "คีย์ API",
"provider.connect.apiKey.required": "ต้องใช้คีย์ API",
"provider.connect.opencodeZen.line1":
"OpenCode Zen ให้คุณเข้าถึงชุดโมเดลที่เชื่อถือได้และปรับแต่งแล้วสำหรับเอเจนต์การเขียนโค้ด",
"provider.connect.opencodeZen.line2":
"ด้วยคีย์ API เดียวคุณจะได้รับการเข้าถึงโมเดล เช่น Claude, GPT, Gemini, GLM และอื่น ๆ",
"provider.connect.opencodeZen.visit.prefix": "เยี่ยมชม ",
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
"provider.connect.opencodeZen.visit.suffix": " เพื่อรวบรวมคีย์ API ของคุณ",
"provider.connect.oauth.code.visit.prefix": "เยี่ยมชม ",
"provider.connect.oauth.code.visit.link": "ลิงก์นี้",
"provider.connect.oauth.code.visit.suffix":
" เพื่อรวบรวมรหัสการอนุญาตของคุณเพื่อเชื่อมต่อบัญชีและใช้โมเดล {{provider}} ใน OpenCode",
"provider.connect.oauth.code.label": "รหัสการอนุญาต {{method}}",
"provider.connect.oauth.code.placeholder": "รหัสการอนุญาต",
"provider.connect.oauth.code.required": "ต้องใช้รหัสการอนุญาต",
"provider.connect.oauth.code.invalid": "รหัสการอนุญาตไม่ถูกต้อง",
"provider.connect.oauth.auto.visit.prefix": "เยี่ยมชม ",
"provider.connect.oauth.auto.visit.link": "ลิงก์นี้",
"provider.connect.oauth.auto.visit.suffix":
" และป้อนรหัสด้านล่างเพื่อเชื่อมต่อบัญชีและใช้โมเดล {{provider}} ใน OpenCode",
"provider.connect.oauth.auto.confirmationCode": "รหัสยืนยัน",
"provider.connect.toast.connected.title": "{{provider}} ที่เชื่อมต่อแล้ว",
"provider.connect.toast.connected.description": "โมเดล {{provider}} พร้อมใช้งานแล้ว",
"provider.disconnect.toast.disconnected.title": "{{provider}} ที่ยกเลิกการเชื่อมต่อแล้ว",
"provider.disconnect.toast.disconnected.description": "โมเดล {{provider}} ไม่พร้อมใช้งานอีกต่อไป",
"model.tag.free": "ฟรี",
"model.tag.latest": "ล่าสุด",
"model.provider.anthropic": "Anthropic",
"model.provider.openai": "OpenAI",
"model.provider.google": "Google",
"model.provider.xai": "xAI",
"model.provider.meta": "Meta",
"model.input.text": "ข้อความ",
"model.input.image": "รูปภาพ",
"model.input.audio": "เสียง",
"model.input.video": "วิดีโอ",
"model.input.pdf": "pdf",
"model.tooltip.allows": "อนุญาต: {{inputs}}",
"model.tooltip.reasoning.allowed": "อนุญาตการใช้เหตุผล",
"model.tooltip.reasoning.none": "ไม่มีการใช้เหตุผล",
"model.tooltip.context": "ขีดจำกัดบริบท {{limit}}",
"common.search.placeholder": "ค้นหา",
"common.goBack": "ย้อนกลับ",
"common.loading": "กำลังโหลด",
"common.loading.ellipsis": "...",
"common.cancel": "ยกเลิก",
"common.connect": "เชื่อมต่อ",
"common.disconnect": "ยกเลิกการเชื่อมต่อ",
"common.submit": "ส่ง",
"common.save": "บันทึก",
"common.saving": "กำลังบันทึก...",
"common.default": "ค่าเริ่มต้น",
"common.attachment": "ไฟล์แนบ",
"prompt.placeholder.shell": "ป้อนคำสั่งเชลล์...",
"prompt.placeholder.normal": 'ถามอะไรก็ได้... "{{example}}"',
"prompt.placeholder.summarizeComments": "สรุปความคิดเห็น…",
"prompt.placeholder.summarizeComment": "สรุปความคิดเห็น…",
"prompt.mode.shell": "เชลล์",
"prompt.mode.shell.exit": "กด esc เพื่อออก",
"prompt.example.1": "แก้ไข TODO ในโค้ดเบส",
"prompt.example.2": "เทคโนโลยีของโปรเจกต์นี้คืออะไร?",
"prompt.example.3": "แก้ไขการทดสอบที่เสีย",
"prompt.example.4": "อธิบายวิธีการทำงานของการตรวจสอบสิทธิ์",
"prompt.example.5": "ค้นหาและแก้ไขช่องโหว่ความปลอดภัย",
"prompt.example.6": "เพิ่มการทดสอบหน่วยสำหรับบริการผู้ใช้",
"prompt.example.7": "ปรับโครงสร้างฟังก์ชันนี้ให้อ่านง่ายขึ้น",
"prompt.example.8": "ข้อผิดพลาดนี้หมายความว่าอะไร?",
"prompt.example.9": "ช่วยฉันดีบักปัญหานี้",
"prompt.example.10": "สร้างเอกสาร API",
"prompt.example.11": "ปรับปรุงการสืบค้นฐานข้อมูล",
"prompt.example.12": "เพิ่มการตรวจสอบข้อมูลนำเข้า",
"prompt.example.13": "สร้างคอมโพเนนต์ใหม่สำหรับ...",
"prompt.example.14": "ฉันจะทำให้โปรเจกต์นี้ทำงานได้อย่างไร?",
"prompt.example.15": "ตรวจสอบโค้ดของฉันเพื่อแนวทางปฏิบัติที่ดีที่สุด",
"prompt.example.16": "เพิ่มการจัดการข้อผิดพลาดในฟังก์ชันนี้",
"prompt.example.17": "อธิบายรูปแบบ regex นี้",
"prompt.example.18": "แปลงสิ่งนี้เป็น TypeScript",
"prompt.example.19": "เพิ่มการบันทึกทั่วทั้งโค้ดเบส",
"prompt.example.20": "มีการพึ่งพาอะไรที่ล้าสมัยอยู่?",
"prompt.example.21": "ช่วยฉันเขียนสคริปต์การย้ายข้อมูล",
"prompt.example.22": "ใช้งานแคชสำหรับจุดสิ้นสุดนี้",
"prompt.example.23": "เพิ่มการแบ่งหน้าในรายการนี้",
"prompt.example.24": "สร้างคำสั่ง CLI สำหรับ...",
"prompt.example.25": "ตัวแปรสภาพแวดล้อมทำงานอย่างไรที่นี่?",
"prompt.popover.emptyResults": "ไม่พบผลลัพธ์ที่ตรงกัน",
"prompt.popover.emptyCommands": "ไม่พบคำสั่งที่ตรงกัน",
"prompt.dropzone.label": "วางรูปภาพหรือ PDF ที่นี่",
"prompt.slash.badge.custom": "กำหนดเอง",
"prompt.context.active": "ใช้งานอยู่",
"prompt.context.includeActiveFile": "รวมไฟล์ที่ใช้งานอยู่",
"prompt.context.removeActiveFile": "เอาไฟล์ที่ใช้งานอยู่ออกจากบริบท",
"prompt.context.removeFile": "เอาไฟล์ออกจากบริบท",
"prompt.action.attachFile": "แนบไฟล์",
"prompt.attachment.remove": "เอาไฟล์แนบออก",
"prompt.action.send": "ส่ง",
"prompt.action.stop": "หยุด",
"prompt.toast.pasteUnsupported.title": "การวางไม่รองรับ",
"prompt.toast.pasteUnsupported.description": "สามารถวางรูปภาพหรือ PDF เท่านั้น",
"prompt.toast.modelAgentRequired.title": "เลือกเอเจนต์และโมเดล",
"prompt.toast.modelAgentRequired.description": "เลือกเอเจนต์และโมเดลก่อนส่งพร้อมท์",
"prompt.toast.worktreeCreateFailed.title": "ไม่สามารถสร้าง worktree",
"prompt.toast.sessionCreateFailed.title": "ไม่สามารถสร้างเซสชัน",
"prompt.toast.shellSendFailed.title": "ไม่สามารถส่งคำสั่งเชลล์",
"prompt.toast.commandSendFailed.title": "ไม่สามารถส่งคำสั่ง",
"prompt.toast.promptSendFailed.title": "ไม่สามารถส่งพร้อมท์",
"dialog.mcp.title": "MCPs",
"dialog.mcp.description": "{{enabled}} จาก {{total}} ที่เปิดใช้งาน",
"dialog.mcp.empty": "ไม่มี MCP ที่กำหนดค่า",
"dialog.lsp.empty": "LSPs ตรวจจับอัตโนมัติจากประเภทไฟล์",
"dialog.plugins.empty": "ปลั๊กอินที่กำหนดค่าใน opencode.json",
"mcp.status.connected": "เชื่อมต่อแล้ว",
"mcp.status.failed": "ล้มเหลว",
"mcp.status.needs_auth": "ต้องการการตรวจสอบสิทธิ์",
"mcp.status.disabled": "ปิดใช้งาน",
"dialog.fork.empty": "ไม่มีข้อความให้แตกแขนง",
"dialog.directory.search.placeholder": "ค้นหาโฟลเดอร์",
"dialog.directory.empty": "ไม่พบโฟลเดอร์",
"dialog.server.title": "เซิร์ฟเวอร์",
"dialog.server.description": "สลับเซิร์ฟเวอร์ OpenCode ที่แอปนี้เชื่อมต่อด้วย",
"dialog.server.search.placeholder": "ค้นหาเซิร์ฟเวอร์",
"dialog.server.empty": "ยังไม่มีเซิร์ฟเวอร์",
"dialog.server.add.title": "เพิ่มเซิร์ฟเวอร์",
"dialog.server.add.url": "URL เซิร์ฟเวอร์",
"dialog.server.add.placeholder": "http://localhost:4096",
"dialog.server.add.error": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์",
"dialog.server.add.checking": "กำลังตรวจสอบ...",
"dialog.server.add.button": "เพิ่มเซิร์ฟเวอร์",
"dialog.server.default.title": "เซิร์ฟเวอร์เริ่มต้น",
"dialog.server.default.description":
"เชื่อมต่อกับเซิร์ฟเวอร์นี้เมื่อเปิดแอปแทนการเริ่มเซิร์ฟเวอร์ในเครื่อง ต้องรีสตาร์ท",
"dialog.server.default.none": "ไม่ได้เลือกเซิร์ฟเวอร์",
"dialog.server.default.set": "ตั้งเซิร์ฟเวอร์ปัจจุบันเป็นค่าเริ่มต้น",
"dialog.server.default.clear": "ล้าง",
"dialog.server.action.remove": "เอาเซิร์ฟเวอร์ออก",
"dialog.server.menu.edit": "แก้ไข",
"dialog.server.menu.default": "ตั้งเป็นค่าเริ่มต้น",
"dialog.server.menu.defaultRemove": "เอาค่าเริ่มต้นออก",
"dialog.server.menu.delete": "ลบ",
"dialog.server.current": "เซิร์ฟเวอร์ปัจจุบัน",
"dialog.server.status.default": "ค่าเริ่มต้น",
"dialog.project.edit.title": "แก้ไขโปรเจกต์",
"dialog.project.edit.name": "ชื่อ",
"dialog.project.edit.icon": "ไอคอน",
"dialog.project.edit.icon.alt": "ไอคอนโปรเจกต์",
"dialog.project.edit.icon.hint": "คลิกหรือลากรูปภาพ",
"dialog.project.edit.icon.recommended": "แนะนำ: 128x128px",
"dialog.project.edit.color": "สี",
"dialog.project.edit.color.select": "เลือกสี {{color}}",
"dialog.project.edit.worktree.startup": "สคริปต์เริ่มต้นพื้นที่ทำงาน",
"dialog.project.edit.worktree.startup.description": "ทำงานหลังจากสร้างพื้นที่ทำงานใหม่ (worktree)",
"dialog.project.edit.worktree.startup.placeholder": "เช่น bun install",
"context.breakdown.title": "การแบ่งบริบท",
"context.breakdown.note": 'การแบ่งโดยประมาณของโทเค็นนำเข้า "อื่น ๆ" รวมถึงคำนิยามเครื่องมือและโอเวอร์เฮด',
"context.breakdown.system": "ระบบ",
"context.breakdown.user": "ผู้ใช้",
"context.breakdown.assistant": "ผู้ช่วย",
"context.breakdown.tool": "การเรียกเครื่องมือ",
"context.breakdown.other": "อื่น ๆ",
"context.systemPrompt.title": "พร้อมท์ระบบ",
"context.rawMessages.title": "ข้อความดิบ",
"context.stats.session": "เซสชัน",
"context.stats.messages": "ข้อความ",
"context.stats.provider": "ผู้ให้บริการ",
"context.stats.model": "โมเดล",
"context.stats.limit": "ขีดจำกัดบริบท",
"context.stats.totalTokens": "โทเค็นทั้งหมด",
"context.stats.usage": "การใช้งาน",
"context.stats.inputTokens": "โทเค็นนำเข้า",
"context.stats.outputTokens": "โทเค็นส่งออก",
"context.stats.reasoningTokens": "โทเค็นการใช้เหตุผล",
"context.stats.cacheTokens": "โทเค็นแคช (อ่าน/เขียน)",
"context.stats.userMessages": "ข้อความผู้ใช้",
"context.stats.assistantMessages": "ข้อความผู้ช่วย",
"context.stats.totalCost": "ต้นทุนทั้งหมด",
"context.stats.sessionCreated": "สร้างเซสชันเมื่อ",
"context.stats.lastActivity": "กิจกรรมล่าสุด",
"context.usage.tokens": "โทเค็น",
"context.usage.usage": "การใช้งาน",
"context.usage.cost": "ต้นทุน",
"context.usage.clickToView": "คลิกเพื่อดูบริบท",
"context.usage.view": "ดูการใช้บริบท",
"language.en": "อังกฤษ",
"language.zh": "จีนตัวย่อ",
"language.zht": "จีนตัวเต็ม",
"language.ko": "เกาหลี",
"language.de": "เยอรมัน",
"language.es": "สเปน",
"language.fr": "ฝรั่งเศส",
"language.da": "เดนมาร์ก",
"language.ja": "ญี่ปุ่น",
"language.pl": "โปแลนด์",
"language.ru": "รัสเซีย",
"language.ar": "อาหรับ",
"language.no": "นอร์เวย์",
"language.br": "โปรตุเกส (บราซิล)",
"language.th": "ไทย",
"toast.language.title": "ภาษา",
"toast.language.description": "สลับไปที่ {{language}}",
"toast.theme.title": "สลับธีมแล้ว",
"toast.scheme.title": "โทนสี",
"toast.permissions.autoaccept.on.title": "กำลังยอมรับการแก้ไขโดยอัตโนมัติ",
"toast.permissions.autoaccept.on.description": "สิทธิ์การแก้ไขและเขียนจะได้รับการอนุมัติโดยอัตโนมัติ",
"toast.permissions.autoaccept.off.title": "หยุดยอมรับการแก้ไขโดยอัตโนมัติ",
"toast.permissions.autoaccept.off.description": "สิทธิ์การแก้ไขและเขียนจะต้องได้รับการอนุมัติ",
"toast.model.none.title": "ไม่ได้เลือกโมเดล",
"toast.model.none.description": "เชื่อมต่อผู้ให้บริการเพื่อสรุปเซสชันนี้",
"toast.file.loadFailed.title": "ไม่สามารถโหลดไฟล์",
"toast.file.listFailed.title": "ไม่สามารถแสดงรายการไฟล์",
"toast.context.noLineSelection.title": "ไม่มีการเลือกบรรทัด",
"toast.context.noLineSelection.description": "เลือกช่วงบรรทัดในแท็บไฟล์ก่อน",
"toast.session.share.copyFailed.title": "ไม่สามารถคัดลอก URL ไปยังคลิปบอร์ด",
"toast.session.share.success.title": "แชร์เซสชันแล้ว",
"toast.session.share.success.description": "คัดลอก URL แชร์ไปยังคลิปบอร์ดแล้ว!",
"toast.session.share.failed.title": "ไม่สามารถแชร์เซสชัน",
"toast.session.share.failed.description": "เกิดข้อผิดพลาดระหว่างการแชร์เซสชัน",
"toast.session.unshare.success.title": "ยกเลิกการแชร์เซสชันแล้ว",
"toast.session.unshare.success.description": "ยกเลิกการแชร์เซสชันสำเร็จ!",
"toast.session.unshare.failed.title": "ไม่สามารถยกเลิกการแชร์เซสชัน",
"toast.session.unshare.failed.description": "เกิดข้อผิดพลาดระหว่างการยกเลิกการแชร์เซสชัน",
"toast.session.listFailed.title": "ไม่สามารถโหลดเซสชันสำหรับ {{project}}",
"toast.update.title": "มีการอัปเดต",
"toast.update.description": "เวอร์ชันใหม่ของ OpenCode ({{version}}) พร้อมใช้งานสำหรับติดตั้ง",
"toast.update.action.installRestart": "ติดตั้งและรีสตาร์ท",
"toast.update.action.notYet": "ยังไม่",
"error.page.title": "เกิดข้อผิดพลาด",
"error.page.description": "เกิดข้อผิดพลาดระหว่างการโหลดแอปพลิเคชัน",
"error.page.details.label": "รายละเอียดข้อผิดพลาด",
"error.page.action.restart": "รีสตาร์ท",
"error.page.action.checking": "กำลังตรวจสอบ...",
"error.page.action.checkUpdates": "ตรวจสอบการอัปเดต",
"error.page.action.updateTo": "อัปเดตเป็น {{version}}",
"error.page.report.prefix": "โปรดรายงานข้อผิดพลาดนี้ให้ทีม OpenCode",
"error.page.report.discord": "บน Discord",
"error.page.version": "เวอร์ชัน: {{version}}",
"error.dev.rootNotFound": "ไม่พบองค์ประกอบรูท คุณลืมเพิ่มใน index.html หรือบางทีแอตทริบิวต์ id อาจสะกดผิด?",
"error.globalSync.connectFailed": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ มีเซิร์ฟเวอร์ทำงานอยู่ที่ `{{url}}` หรือไม่?",
"error.chain.unknown": "ข้อผิดพลาดที่ไม่รู้จัก",
"error.chain.causedBy": "สาเหตุ:",
"error.chain.apiError": "ข้อผิดพลาด API",
"error.chain.status": "สถานะ: {{status}}",
"error.chain.retryable": "สามารถลองใหม่: {{retryable}}",
"error.chain.responseBody": "เนื้อหาการตอบสนอง:\n{{body}}",
"error.chain.didYouMean": "คุณหมายถึง: {{suggestions}}",
"error.chain.modelNotFound": "ไม่พบโมเดล: {{provider}}/{{model}}",
"error.chain.checkConfig": "ตรวจสอบการกำหนดค่าของคุณ (opencode.json) ชื่อผู้ให้บริการ/โมเดล",
"error.chain.mcpFailed": 'เซิร์ฟเวอร์ MCP "{{name}}" ล้มเหลว โปรดทราบว่า OpenCode ยังไม่รองรับการตรวจสอบสิทธิ์ MCP',
"error.chain.providerAuthFailed": "การตรวจสอบสิทธิ์ผู้ให้บริการล้มเหลว ({{provider}}): {{message}}",
"error.chain.providerInitFailed": 'ไม่สามารถเริ่มต้นผู้ให้บริการ "{{provider}}" ตรวจสอบข้อมูลรับรองและการกำหนดค่า',
"error.chain.configJsonInvalid": "ไฟล์กำหนดค่าที่ {{path}} ไม่ใช่ JSON(C) ที่ถูกต้อง",
"error.chain.configJsonInvalidWithMessage": "ไฟล์กำหนดค่าที่ {{path}} ไม่ใช่ JSON(C) ที่ถูกต้อง: {{message}}",
"error.chain.configDirectoryTypo":
'ไดเรกทอรี "{{dir}}" ใน {{path}} ไม่ถูกต้อง เปลี่ยนชื่อไดเรกทอรีเป็น "{{suggestion}}" หรือเอาออก นี่เป็นการสะกดผิดทั่วไป',
"error.chain.configFrontmatterError": "ไม่สามารถแยกวิเคราะห์ frontmatter ใน {{path}}:\n{{message}}",
"error.chain.configInvalid": "ไฟล์กำหนดค่าที่ {{path}} ไม่ถูกต้อง",
"error.chain.configInvalidWithMessage": "ไฟล์กำหนดค่าที่ {{path}} ไม่ถูกต้อง: {{message}}",
"notification.permission.title": "ต้องการสิทธิ์",
"notification.permission.description": "{{sessionTitle}} ใน {{projectName}} ต้องการสิทธิ์",
"notification.question.title": "คำถาม",
"notification.question.description": "{{sessionTitle}} ใน {{projectName}} มีคำถาม",
"notification.action.goToSession": "ไปที่เซสชัน",
"notification.session.responseReady.title": "การตอบสนองพร้อม",
"notification.session.error.title": "ข้อผิดพลาดเซสชัน",
"notification.session.error.fallbackDescription": "เกิดข้อผิดพลาด",
"home.recentProjects": "โปรเจกต์ล่าสุด",
"home.empty.title": "ไม่มีโปรเจกต์ล่าสุด",
"home.empty.description": "เริ่มต้นโดยเปิดโปรเจกต์ในเครื่อง",
"session.tab.session": "เซสชัน",
"session.tab.review": "ตรวจสอบ",
"session.tab.context": "บริบท",
"session.panel.reviewAndFiles": "ตรวจสอบและไฟล์",
"session.review.filesChanged": "{{count}} ไฟล์ที่เปลี่ยนแปลง",
"session.review.change.one": "การเปลี่ยนแปลง",
"session.review.change.other": "การเปลี่ยนแปลง",
"session.review.loadingChanges": "กำลังโหลดการเปลี่ยนแปลง...",
"session.review.empty": "ยังไม่มีการเปลี่ยนแปลงในเซสชันนี้",
"session.review.noChanges": "ไม่มีการเปลี่ยนแปลง",
"session.files.selectToOpen": "เลือกไฟล์เพื่อเปิด",
"session.files.all": "ไฟล์ทั้งหมด",
"session.messages.renderEarlier": "แสดงข้อความก่อนหน้า",
"session.messages.loadingEarlier": "กำลังโหลดข้อความก่อนหน้า...",
"session.messages.loadEarlier": "โหลดข้อความก่อนหน้า",
"session.messages.loading": "กำลังโหลดข้อความ...",
"session.messages.jumpToLatest": "ไปที่ล่าสุด",
"session.context.addToContext": "เพิ่ม {{selection}} ไปยังบริบท",
"session.new.worktree.main": "สาขาหลัก",
"session.new.worktree.mainWithBranch": "สาขาหลัก ({{branch}})",
"session.new.worktree.create": "สร้าง worktree ใหม่",
"session.new.lastModified": "แก้ไขล่าสุด",
"session.header.search.placeholder": "ค้นหา {{project}}",
"session.header.searchFiles": "ค้นหาไฟล์",
"status.popover.trigger": "สถานะ",
"status.popover.ariaLabel": "การกำหนดค่าเซิร์ฟเวอร์",
"status.popover.tab.servers": "เซิร์ฟเวอร์",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "ปลั๊กอิน",
"status.popover.action.manageServers": "จัดการเซิร์ฟเวอร์",
"session.share.popover.title": "เผยแพร่บนเว็บ",
"session.share.popover.description.shared": "เซสชันนี้เป็นสาธารณะบนเว็บ สามารถเข้าถึงได้โดยผู้ที่มีลิงก์",
"session.share.popover.description.unshared": "แชร์เซสชันสาธารณะบนเว็บ จะเข้าถึงได้โดยผู้ที่มีลิงก์",
"session.share.action.share": "แชร์",
"session.share.action.publish": "เผยแพร่",
"session.share.action.publishing": "กำลังเผยแพร่...",
"session.share.action.unpublish": "ยกเลิกการเผยแพร่",
"session.share.action.unpublishing": "กำลังยกเลิกการเผยแพร่...",
"session.share.action.view": "ดู",
"session.share.copy.copied": "คัดลอกแล้ว",
"session.share.copy.copyLink": "คัดลอกลิงก์",
"lsp.tooltip.none": "ไม่มีเซิร์ฟเวอร์ LSP",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "กำลังโหลดพร้อมท์...",
"terminal.loading": "กำลังโหลดเทอร์มินัล...",
"terminal.title": "เทอร์มินัล",
"terminal.title.numbered": "เทอร์มินัล {{number}}",
"terminal.close": "ปิดเทอร์มินัล",
"terminal.connectionLost.title": "การเชื่อมต่อขาดหาย",
"terminal.connectionLost.description": "การเชื่อมต่อเทอร์มินัลถูกขัดจังหวะ อาจเกิดขึ้นเมื่อเซิร์ฟเวอร์รีสตาร์ท",
"common.closeTab": "ปิดแท็บ",
"common.dismiss": "ปิด",
"common.requestFailed": "คำขอล้มเหลว",
"common.moreOptions": "ตัวเลือกเพิ่มเติม",
"common.learnMore": "เรียนรู้เพิ่มเติม",
"common.rename": "เปลี่ยนชื่อ",
"common.reset": "รีเซ็ต",
"common.archive": "จัดเก็บ",
"common.delete": "ลบ",
"common.close": "ปิด",
"common.edit": "แก้ไข",
"common.loadMore": "โหลดเพิ่มเติม",
"common.key.esc": "ESC",
"sidebar.menu.toggle": "สลับเมนู",
"sidebar.nav.projectsAndSessions": "โปรเจกต์และเซสชัน",
"sidebar.settings": "การตั้งค่า",
"sidebar.help": "ช่วยเหลือ",
"sidebar.workspaces.enable": "เปิดใช้งานพื้นที่ทำงาน",
"sidebar.workspaces.disable": "ปิดใช้งานพื้นที่ทำงาน",
"sidebar.gettingStarted.title": "เริ่มต้นใช้งาน",
"sidebar.gettingStarted.line1": "OpenCode รวมถึงโมเดลฟรีเพื่อให้คุณเริ่มต้นได้ทันที",
"sidebar.gettingStarted.line2": "เชื่อมต่อผู้ให้บริการใด ๆ เพื่อใช้โมเดล รวมถึง Claude, GPT, Gemini ฯลฯ",
"sidebar.project.recentSessions": "เซสชันล่าสุด",
"sidebar.project.viewAllSessions": "ดูเซสชันทั้งหมด",
"app.name.desktop": "OpenCode Desktop",
"settings.section.desktop": "เดสก์ท็อป",
"settings.section.server": "เซิร์ฟเวอร์",
"settings.tab.general": "ทั่วไป",
"settings.tab.shortcuts": "ทางลัด",
"settings.general.section.appearance": "รูปลักษณ์",
"settings.general.section.notifications": "การแจ้งเตือนระบบ",
"settings.general.section.updates": "การอัปเดต",
"settings.general.section.sounds": "เสียงเอฟเฟกต์",
"settings.general.row.language.title": "ภาษา",
"settings.general.row.language.description": "เปลี่ยนภาษาที่แสดงสำหรับ OpenCode",
"settings.general.row.appearance.title": "รูปลักษณ์",
"settings.general.row.appearance.description": "ปรับแต่งวิธีการที่ OpenCode มีลักษณะบนอุปกรณ์ของคุณ",
"settings.general.row.theme.title": "ธีม",
"settings.general.row.theme.description": "ปรับแต่งวิธีการที่ OpenCode มีธีม",
"settings.general.row.font.title": "ฟอนต์",
"settings.general.row.font.description": "ปรับแต่งฟอนต์โมโนที่ใช้ในบล็อกโค้ด",
"settings.general.row.releaseNotes.title": "บันทึกการอัปเดต",
"settings.general.row.releaseNotes.description": "แสดงป๊อปอัพ What's New หลังจากอัปเดต",
"font.option.ibmPlexMono": "IBM Plex Mono",
"font.option.cascadiaCode": "Cascadia Code",
"font.option.firaCode": "Fira Code",
"font.option.hack": "Hack",
"font.option.inconsolata": "Inconsolata",
"font.option.intelOneMono": "Intel One Mono",
"font.option.iosevka": "Iosevka",
"font.option.jetbrainsMono": "JetBrains Mono",
"font.option.mesloLgs": "Meslo LGS",
"font.option.robotoMono": "Roboto Mono",
"font.option.sourceCodePro": "Source Code Pro",
"font.option.ubuntuMono": "Ubuntu Mono",
"sound.option.alert01": "เสียงเตือน 01",
"sound.option.alert02": "เสียงเตือน 02",
"sound.option.alert03": "เสียงเตือน 03",
"sound.option.alert04": "เสียงเตือน 04",
"sound.option.alert05": "เสียงเตือน 05",
"sound.option.alert06": "เสียงเตือน 06",
"sound.option.alert07": "เสียงเตือน 07",
"sound.option.alert08": "เสียงเตือน 08",
"sound.option.alert09": "เสียงเตือน 09",
"sound.option.alert10": "เสียงเตือน 10",
"sound.option.bipbop01": "Bip-bop 01",
"sound.option.bipbop02": "Bip-bop 02",
"sound.option.bipbop03": "Bip-bop 03",
"sound.option.bipbop04": "Bip-bop 04",
"sound.option.bipbop05": "Bip-bop 05",
"sound.option.bipbop06": "Bip-bop 06",
"sound.option.bipbop07": "Bip-bop 07",
"sound.option.bipbop08": "Bip-bop 08",
"sound.option.bipbop09": "Bip-bop 09",
"sound.option.bipbop10": "Bip-bop 10",
"sound.option.staplebops01": "Staplebops 01",
"sound.option.staplebops02": "Staplebops 02",
"sound.option.staplebops03": "Staplebops 03",
"sound.option.staplebops04": "Staplebops 04",
"sound.option.staplebops05": "Staplebops 05",
"sound.option.staplebops06": "Staplebops 06",
"sound.option.staplebops07": "Staplebops 07",
"sound.option.nope01": "Nope 01",
"sound.option.nope02": "Nope 02",
"sound.option.nope03": "Nope 03",
"sound.option.nope04": "Nope 04",
"sound.option.nope05": "Nope 05",
"sound.option.nope06": "Nope 06",
"sound.option.nope07": "Nope 07",
"sound.option.nope08": "Nope 08",
"sound.option.nope09": "Nope 09",
"sound.option.nope10": "Nope 10",
"sound.option.nope11": "Nope 11",
"sound.option.nope12": "Nope 12",
"sound.option.yup01": "Yup 01",
"sound.option.yup02": "Yup 02",
"sound.option.yup03": "Yup 03",
"sound.option.yup04": "Yup 04",
"sound.option.yup05": "Yup 05",
"sound.option.yup06": "Yup 06",
"settings.general.notifications.agent.title": "เอเจนต์",
"settings.general.notifications.agent.description": "แสดงการแจ้งเตือนระบบเมื่อเอเจนต์เสร็จสิ้นหรือต้องการความสนใจ",
"settings.general.notifications.permissions.title": "สิทธิ์",
"settings.general.notifications.permissions.description": "แสดงการแจ้งเตือนระบบเมื่อต้องการสิทธิ์",
"settings.general.notifications.errors.title": "ข้อผิดพลาด",
"settings.general.notifications.errors.description": "แสดงการแจ้งเตือนระบบเมื่อเกิดข้อผิดพลาด",
"settings.general.sounds.agent.title": "เอเจนต์",
"settings.general.sounds.agent.description": "เล่นเสียงเมื่อเอเจนต์เสร็จสิ้นหรือต้องการความสนใจ",
"settings.general.sounds.permissions.title": "สิทธิ์",
"settings.general.sounds.permissions.description": "เล่นเสียงเมื่อต้องการสิทธิ์",
"settings.general.sounds.errors.title": "ข้อผิดพลาด",
"settings.general.sounds.errors.description": "เล่นเสียงเมื่อเกิดข้อผิดพลาด",
"settings.shortcuts.title": "ทางลัดแป้นพิมพ์",
"settings.shortcuts.reset.button": "รีเซ็ตเป็นค่าเริ่มต้น",
"settings.shortcuts.reset.toast.title": "รีเซ็ตทางลัดแล้ว",
"settings.shortcuts.reset.toast.description": "รีเซ็ตทางลัดแป้นพิมพ์เป็นค่าเริ่มต้นแล้ว",
"settings.shortcuts.conflict.title": "ทางลัดใช้งานอยู่แล้ว",
"settings.shortcuts.conflict.description": "{{keybind}} ถูกกำหนดให้กับ {{titles}} แล้ว",
"settings.shortcuts.unassigned": "ไม่ได้กำหนด",
"settings.shortcuts.pressKeys": "กดปุ่ม",
"settings.shortcuts.search.placeholder": "ค้นหาทางลัด",
"settings.shortcuts.search.empty": "ไม่พบทางลัด",
"settings.shortcuts.group.general": "ทั่วไป",
"settings.shortcuts.group.session": "เซสชัน",
"settings.shortcuts.group.navigation": "การนำทาง",
"settings.shortcuts.group.modelAndAgent": "โมเดลและเอเจนต์",
"settings.shortcuts.group.terminal": "เทอร์มินัล",
"settings.shortcuts.group.prompt": "พร้อมท์",
"settings.providers.title": "ผู้ให้บริการ",
"settings.providers.description": "การตั้งค่าผู้ให้บริการจะสามารถกำหนดค่าได้ที่นี่",
"settings.providers.section.connected": "ผู้ให้บริการที่เชื่อมต่อ",
"settings.providers.connected.empty": "ไม่มีผู้ให้บริการที่เชื่อมต่อ",
"settings.providers.section.popular": "ผู้ให้บริการยอดนิยม",
"settings.providers.tag.environment": "สภาพแวดล้อม",
"settings.providers.tag.config": "กำหนดค่า",
"settings.providers.tag.custom": "กำหนดเอง",
"settings.providers.tag.other": "อื่น ๆ",
"settings.models.title": "โมเดล",
"settings.models.description": "การตั้งค่าโมเดลจะสามารถกำหนดค่าได้ที่นี่",
"settings.agents.title": "เอเจนต์",
"settings.agents.description": "การตั้งค่าเอเจนต์จะสามารถกำหนดค่าได้ที่นี่",
"settings.commands.title": "คำสั่ง",
"settings.commands.description": "การตั้งค่าคำสั่งจะสามารถกำหนดค่าได้ที่นี่",
"settings.mcp.title": "MCP",
"settings.mcp.description": "การตั้งค่า MCP จะสามารถกำหนดค่าได้ที่นี่",
"settings.permissions.title": "สิทธิ์",
"settings.permissions.description": "ควบคุมเครื่องมือที่เซิร์ฟเวอร์สามารถใช้โดยค่าเริ่มต้น",
"settings.permissions.section.tools": "เครื่องมือ",
"settings.permissions.toast.updateFailed.title": "ไม่สามารถอัปเดตสิทธิ์",
"settings.permissions.action.allow": "อนุญาต",
"settings.permissions.action.ask": "ถาม",
"settings.permissions.action.deny": "ปฏิเสธ",
"settings.permissions.tool.read.title": "อ่าน",
"settings.permissions.tool.read.description": "อ่านไฟล์ (ตรงกับเส้นทางไฟล์)",
"settings.permissions.tool.edit.title": "แก้ไข",
"settings.permissions.tool.edit.description": "แก้ไขไฟล์ รวมถึงการแก้ไข เขียน แพตช์ และแก้ไขหลายรายการ",
"settings.permissions.tool.glob.title": "Glob",
"settings.permissions.tool.glob.description": "จับคู่ไฟล์โดยใช้รูปแบบ glob",
"settings.permissions.tool.grep.title": "Grep",
"settings.permissions.tool.grep.description": "ค้นหาเนื้อหาไฟล์โดยใช้นิพจน์ทั่วไป",
"settings.permissions.tool.list.title": "รายการ",
"settings.permissions.tool.list.description": "แสดงรายการไฟล์ภายในไดเรกทอรี",
"settings.permissions.tool.bash.title": "Bash",
"settings.permissions.tool.bash.description": "เรียกใช้คำสั่งเชลล์",
"settings.permissions.tool.task.title": "งาน",
"settings.permissions.tool.task.description": "เปิดเอเจนต์ย่อย",
"settings.permissions.tool.skill.title": "ทักษะ",
"settings.permissions.tool.skill.description": "โหลดทักษะตามชื่อ",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "เรียกใช้การสืบค้นเซิร์ฟเวอร์ภาษา",
"settings.permissions.tool.todoread.title": "อ่านรายการงาน",
"settings.permissions.tool.todoread.description": "อ่านรายการงาน",
"settings.permissions.tool.todowrite.title": "เขียนรายการงาน",
"settings.permissions.tool.todowrite.description": "อัปเดตรายการงาน",
"settings.permissions.tool.webfetch.title": "ดึงข้อมูลจากเว็บ",
"settings.permissions.tool.webfetch.description": "ดึงเนื้อหาจาก URL",
"settings.permissions.tool.websearch.title": "ค้นหาเว็บ",
"settings.permissions.tool.websearch.description": "ค้นหาบนเว็บ",
"settings.permissions.tool.codesearch.title": "ค้นหาโค้ด",
"settings.permissions.tool.codesearch.description": "ค้นหาโค้ดบนเว็บ",
"settings.permissions.tool.external_directory.title": "ไดเรกทอรีภายนอก",
"settings.permissions.tool.external_directory.description": "เข้าถึงไฟล์นอกไดเรกทอรีโปรเจกต์",
"settings.permissions.tool.doom_loop.title": "Doom Loop",
"settings.permissions.tool.doom_loop.description": "ตรวจจับการเรียกเครื่องมือซ้ำด้วยข้อมูลนำเข้าเหมือนกัน",
"session.delete.failed.title": "ไม่สามารถลบเซสชัน",
"session.delete.title": "ลบเซสชัน",
"session.delete.confirm": 'ลบเซสชัน "{{name}}" หรือไม่?',
"session.delete.button": "ลบเซสชัน",
"workspace.new": "พื้นที่ทำงานใหม่",
"workspace.type.local": "ในเครื่อง",
"workspace.type.sandbox": "แซนด์บ็อกซ์",
"workspace.create.failed.title": "ไม่สามารถสร้างพื้นที่ทำงาน",
"workspace.delete.failed.title": "ไม่สามารถลบพื้นที่ทำงาน",
"workspace.resetting.title": "กำลังรีเซ็ตพื้นที่ทำงาน",
"workspace.resetting.description": "อาจใช้เวลาประมาณหนึ่งนาที",
"workspace.reset.failed.title": "ไม่สามารถรีเซ็ตพื้นที่ทำงาน",
"workspace.reset.success.title": "รีเซ็ตพื้นที่ทำงานแล้ว",
"workspace.reset.success.description": "พื้นที่ทำงานตรงกับสาขาเริ่มต้นแล้ว",
"workspace.error.stillPreparing": "พื้นที่ทำงานกำลังเตรียมอยู่",
"workspace.status.checking": "กำลังตรวจสอบการเปลี่ยนแปลงที่ไม่ได้ผสาน...",
"workspace.status.error": "ไม่สามารถตรวจสอบสถานะ git",
"workspace.status.clean": "ไม่ตรวจพบการเปลี่ยนแปลงที่ไม่ได้ผสาน",
"workspace.status.dirty": "ตรวจพบการเปลี่ยนแปลงที่ไม่ได้ผสานในพื้นที่ทำงานนี้",
"workspace.delete.title": "ลบพื้นที่ทำงาน",
"workspace.delete.confirm": 'ลบพื้นที่ทำงาน "{{name}}" หรือไม่?',
"workspace.delete.button": "ลบพื้นที่ทำงาน",
"workspace.reset.title": "รีเซ็ตพื้นที่ทำงาน",
"workspace.reset.confirm": 'รีเซ็ตพื้นที่ทำงาน "{{name}}" หรือไม่?',
"workspace.reset.button": "รีเซ็ตพื้นที่ทำงาน",
"workspace.reset.archived.none": "ไม่มีเซสชันที่ใช้งานอยู่จะถูกจัดเก็บ",
"workspace.reset.archived.one": "1 เซสชันจะถูกจัดเก็บ",
"workspace.reset.archived.many": "{{count}} เซสชันจะถูกจัดเก็บ",
"workspace.reset.note": "สิ่งนี้จะรีเซ็ตพื้นที่ทำงานให้ตรงกับสาขาเริ่มต้น",
}
+32 -28
View File
@@ -37,12 +37,12 @@ export const dict = {
"command.palette": "命令面板",
"command.theme.cycle": "切换主题",
"command.theme.set": "使用主题: {{theme}}",
"command.theme.set": "使用主题{{theme}}",
"command.theme.scheme.cycle": "切换配色方案",
"command.theme.scheme.set": "使用配色方案: {{scheme}}",
"command.theme.scheme.set": "使用配色方案{{scheme}}",
"command.language.cycle": "切换语言",
"command.language.set": "使用语言: {{language}}",
"command.language.set": "使用语言{{language}}",
"command.session.new": "新建会话",
"command.file.open": "打开文件",
@@ -98,6 +98,10 @@ export const dict = {
"dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 密钥连接",
"dialog.provider.openai.note": "使用 ChatGPT Pro/Plus 或 API 密钥连接",
"dialog.provider.copilot.note": "使用 Copilot 或 API 密钥连接",
"dialog.provider.opencode.note": "使用 OpenCode Zen 或 API 密钥连接",
"dialog.provider.google.note": "使用 Google 账号或 API 密钥连接",
"dialog.provider.openrouter.note": "使用 OpenRouter 账号或 API 密钥连接",
"dialog.provider.vercel.note": "使用 Vercel 账号或 API 密钥连接",
"dialog.model.select.title": "选择模型",
"dialog.model.search.placeholder": "搜索模型",
@@ -116,7 +120,7 @@ export const dict = {
"provider.connect.method.apiKey": "API 密钥",
"provider.connect.status.inProgress": "正在授权...",
"provider.connect.status.waiting": "等待授权...",
"provider.connect.status.failed": "授权失败: {{error}}",
"provider.connect.status.failed": "授权失败{{error}}",
"provider.connect.apiKey.description":
"输入你的 {{provider}} API 密钥以连接帐户,并在 OpenCode 中使用 {{provider}} 模型。",
"provider.connect.apiKey.label": "{{provider}} API 密钥",
@@ -156,7 +160,7 @@ export const dict = {
"model.input.audio": "音频",
"model.input.video": "视频",
"model.input.pdf": "pdf",
"model.tooltip.allows": "支持: {{inputs}}",
"model.tooltip.allows": "支持{{inputs}}",
"model.tooltip.reasoning.allowed": "支持推理",
"model.tooltip.reasoning.none": "不支持推理",
"model.tooltip.context": "上下文上限 {{limit}}",
@@ -181,30 +185,30 @@ export const dict = {
"prompt.mode.shell.exit": "按 esc 退出",
"prompt.example.1": "修复代码库中的一个 TODO",
"prompt.example.2": "这个项目的技术栈是什么?",
"prompt.example.2": "这个项目的技术栈是什么",
"prompt.example.3": "修复失败的测试",
"prompt.example.4": "解释认证是如何工作的",
"prompt.example.5": "查找并修复安全漏洞",
"prompt.example.6": "为用户服务添加单元测试",
"prompt.example.7": "重构这个函数,让它更易读",
"prompt.example.8": "这个错误是什么意思?",
"prompt.example.8": "这个错误是什么意思",
"prompt.example.9": "帮我调试这个问题",
"prompt.example.10": "生成 API 文档",
"prompt.example.11": "优化数据库查询",
"prompt.example.12": "添加输入校验",
"prompt.example.13": "创建一个新的组件用于...",
"prompt.example.14": "我该如何部署这个项目?",
"prompt.example.14": "我该如何部署这个项目",
"prompt.example.15": "审查我的代码并给出最佳实践建议",
"prompt.example.16": "为这个函数添加错误处理",
"prompt.example.17": "解释这个正则表达式",
"prompt.example.18": "把它转换成 TypeScript",
"prompt.example.19": "在整个代码库中添加日志",
"prompt.example.20": "哪些依赖已经过期?",
"prompt.example.20": "哪些依赖已经过期",
"prompt.example.21": "帮我写一个迁移脚本",
"prompt.example.22": "为这个接口实现缓存",
"prompt.example.23": "给这个列表添加分页",
"prompt.example.24": "创建一个 CLI 命令用于...",
"prompt.example.25": "这里的环境变量是怎么工作的?",
"prompt.example.25": "这里的环境变量是怎么工作的",
"prompt.popover.emptyResults": "没有匹配的结果",
"prompt.popover.emptyCommands": "没有匹配的命令",
@@ -330,6 +334,7 @@ export const dict = {
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "语言",
"toast.language.description": "已切换到{{language}}",
@@ -377,31 +382,31 @@ export const dict = {
"error.page.action.updateTo": "更新到 {{version}}",
"error.page.report.prefix": "请将此错误报告给 OpenCode 团队",
"error.page.report.discord": "在 Discord 上",
"error.page.version": "版本: {{version}}",
"error.page.version": "版本{{version}}",
"error.dev.rootNotFound": "未找到根元素。你是不是忘了把它添加到 index.html? 或者 id 属性拼写错了?",
"error.dev.rootNotFound": "未找到根元素。你是不是忘了把它添加到 index.html或者 id 属性拼写错了",
"error.globalSync.connectFailed": "无法连接到服务器。是否有服务器正在 `{{url}}` 运行?",
"error.globalSync.connectFailed": "无法连接到服务器。是否有服务器正在 `{{url}}` 运行",
"error.chain.unknown": "未知错误",
"error.chain.causedBy": "原因:",
"error.chain.causedBy": "原因",
"error.chain.apiError": "API 错误",
"error.chain.status": "状态: {{status}}",
"error.chain.retryable": "可重试: {{retryable}}",
"error.chain.responseBody": "响应内容:\n{{body}}",
"error.chain.didYouMean": "你是不是想输入: {{suggestions}}",
"error.chain.modelNotFound": "未找到模型: {{provider}}/{{model}}",
"error.chain.status": "状态{{status}}",
"error.chain.retryable": "可重试{{retryable}}",
"error.chain.responseBody": "响应内容\n{{body}}",
"error.chain.didYouMean": "你是不是想输入{{suggestions}}",
"error.chain.modelNotFound": "未找到模型{{provider}}/{{model}}",
"error.chain.checkConfig": "请检查你的配置 (opencode.json) 中的 provider/model 名称",
"error.chain.mcpFailed": 'MCP 服务器 "{{name}}" 启动失败。注意: OpenCode 暂不支持 MCP 认证。',
"error.chain.providerAuthFailed": "提供商认证失败 ({{provider}}): {{message}}",
"error.chain.providerAuthFailed": "提供商认证失败{{provider}}):{{message}}",
"error.chain.providerInitFailed": '无法初始化提供商 "{{provider}}"。请检查凭据和配置。',
"error.chain.configJsonInvalid": "配置文件 {{path}} 不是有效的 JSON(C)",
"error.chain.configJsonInvalidWithMessage": "配置文件 {{path}} 不是有效的 JSON(C): {{message}}",
"error.chain.configJsonInvalidWithMessage": "配置文件 {{path}} 不是有效的 JSON(C){{message}}",
"error.chain.configDirectoryTypo":
'{{path}} 中的目录 "{{dir}}" 无效。请将目录重命名为 "{{suggestion}}" 或移除它。这是一个常见拼写错误。',
"error.chain.configFrontmatterError": "无法解析 {{path}} 中的 frontmatter:\n{{message}}",
"error.chain.configFrontmatterError": "无法解析 {{path}} 中的 frontmatter\n{{message}}",
"error.chain.configInvalid": "配置文件 {{path}} 无效",
"error.chain.configInvalidWithMessage": "配置文件 {{path}} 无效: {{message}}",
"error.chain.configInvalidWithMessage": "配置文件 {{path}} 无效{{message}}",
"notification.permission.title": "需要权限",
"notification.permission.description": "{{sessionTitle}}{{projectName}})需要权限",
@@ -438,7 +443,7 @@ export const dict = {
"session.context.addToContext": "将 {{selection}} 添加到上下文",
"session.new.worktree.main": "主分支",
"session.new.worktree.mainWithBranch": "主分支 ({{branch}})",
"session.new.worktree.mainWithBranch": "主分支{{branch}}",
"session.new.worktree.create": "创建新的 worktree",
"session.new.lastModified": "最后修改",
@@ -521,7 +526,6 @@ export const dict = {
"settings.general.row.theme.description": "自定义 OpenCode 的主题。",
"settings.general.row.font.title": "字体",
"settings.general.row.font.description": "自定义代码块使用的等宽字体",
"settings.general.row.releaseNotes.title": "发行说明",
"settings.general.row.releaseNotes.description": "更新后显示“新功能”弹窗",
@@ -685,7 +689,7 @@ export const dict = {
"session.delete.failed.title": "删除会话失败",
"session.delete.title": "删除会话",
"session.delete.confirm": '删除会话 "{{name}}"?',
"session.delete.confirm": '删除会话 "{{name}}"',
"session.delete.button": "删除会话",
"workspace.new": "新建工作区",
@@ -704,10 +708,10 @@ export const dict = {
"workspace.status.clean": "未检测到未合并的更改。",
"workspace.status.dirty": "检测到未合并的更改。",
"workspace.delete.title": "删除工作区",
"workspace.delete.confirm": '删除工作区 "{{name}}"?',
"workspace.delete.confirm": '删除工作区 "{{name}}"',
"workspace.delete.button": "删除工作区",
"workspace.reset.title": "重置工作区",
"workspace.reset.confirm": '重置工作区 "{{name}}"?',
"workspace.reset.confirm": '重置工作区 "{{name}}"',
"workspace.reset.button": "重置工作区",
"workspace.reset.archived.none": "不会归档任何活跃会话。",
"workspace.reset.archived.one": "将归档 1 个会话。",
+1
View File
@@ -331,6 +331,7 @@ export const dict = {
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "語言",
"toast.language.description": "已切換到 {{language}}",
+40
View File
@@ -1136,6 +1136,46 @@ export default function Layout(props: ParentProps) {
if (navigate) navigateToProject(directory)
}
const deepLinkEvent = "opencode:deep-link"
const parseDeepLink = (input: string) => {
if (!input.startsWith("opencode://")) return
const url = new URL(input)
if (url.hostname !== "open-project") return
const directory = url.searchParams.get("directory")
if (!directory) return
return directory
}
const handleDeepLinks = (urls: string[]) => {
if (!server.isLocal()) return
for (const input of urls) {
const directory = parseDeepLink(input)
if (!directory) continue
openProject(directory)
}
}
const drainDeepLinks = () => {
const pending = window.__OPENCODE__?.deepLinks ?? []
if (pending.length === 0) return
if (window.__OPENCODE__) window.__OPENCODE__.deepLinks = []
handleDeepLinks(pending)
}
onMount(() => {
const handler = (event: Event) => {
const detail = (event as CustomEvent<{ urls: string[] }>).detail
const urls = detail?.urls ?? []
if (urls.length === 0) return
handleDeepLinks(urls)
}
drainDeepLinks()
window.addEventListener(deepLinkEvent, handler as EventListener)
onCleanup(() => window.removeEventListener(deepLinkEvent, handler as EventListener))
})
const displayName = (project: LocalProject) => project.name || getFilename(project.worktree)
async function renameProject(project: LocalProject, next: string) {
+86 -66
View File
@@ -23,7 +23,6 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Tabs } from "@opencode-ai/ui/tabs"
import { useCodeComponent } from "@opencode-ai/ui/context/code"
@@ -325,6 +324,7 @@ export default function Page() {
}
const isDesktop = createMediaQuery("(min-width: 768px)")
const centered = createMemo(() => isDesktop() && !layout.fileTree.opened())
function normalizeTab(tab: string) {
if (!tab.startsWith("file://")) return tab
@@ -730,8 +730,8 @@ export default function Page() {
onSelect: () => view().terminal.toggle(),
},
{
id: "fileTree.toggle",
title: language.t("command.fileTree.toggle"),
id: "review.toggle",
title: language.t("command.review.toggle"),
description: "",
category: language.t("command.category.view"),
keybind: "mod+shift+r",
@@ -1097,12 +1097,11 @@ export default function Page() {
}, 0)
}
const reviewTab = createMemo(() => isDesktop() && !layout.fileTree.opened())
const contextOpen = createMemo(() => tabs().active() === "context" || tabs().all().includes("context"))
const openedTabs = createMemo(() =>
tabs()
.all()
.filter((tab) => tab !== "context" && tab !== "review"),
.filter((tab) => tab !== "context"),
)
const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
@@ -1271,56 +1270,29 @@ export default function Page() {
const activeTab = createMemo(() => {
const active = tabs().active()
if (active === "context") return "context"
if (active === "review" && reviewTab()) return "review"
if (active && file.pathFromTab(active)) return normalizeTab(active)
const first = openedTabs()[0]
if (first) return first
if (contextOpen()) return "context"
if (reviewTab() && hasReview()) return "review"
return "empty"
})
createEffect(() => {
if (!layout.ready()) return
if (tabs().active()) return
if (openedTabs().length === 0 && !contextOpen() && !(reviewTab() && hasReview())) return
if (openedTabs().length === 0 && !contextOpen()) return
const next = activeTab()
if (next === "empty") return
tabs().setActive(next)
})
createEffect(
on(
() => layout.fileTree.opened(),
(opened, prev) => {
if (prev === undefined) return
if (!isDesktop()) return
if (opened) {
const active = tabs().active()
const tab = active === "review" || (!active && hasReview()) ? "changes" : "all"
layout.fileTree.setTab(tab)
return
}
if (fileTreeTab() !== "changes") return
tabs().setActive("review")
},
{ defer: true },
),
)
createEffect(() => {
const id = params.id
if (!id) return
const wants = isDesktop()
? layout.fileTree.opened()
? fileTreeTab() === "changes"
: activeTab() === "review"
: store.mobileTab === "changes"
const wants = isDesktop() ? layout.fileTree.opened() && fileTreeTab() === "changes" : store.mobileTab === "changes"
if (!wants) return
if (sync.data.session_diff[id] !== undefined) return
if (sync.status === "loading") return
@@ -1789,10 +1761,11 @@ export default function Page() {
<div
classList={{
"@container relative shrink-0 flex flex-col min-h-0 h-full bg-background-stronger": true,
"flex-1 md:flex-none pt-6 md:pt-3": true,
"flex-1 pt-6 md:pt-3": true,
"md:flex-none": layout.fileTree.opened(),
}}
style={{
width: isDesktop() ? `${layout.session.width()}px` : "100%",
width: isDesktop() && layout.fileTree.opened() ? `${layout.session.width()}px` : "100%",
"--prompt-height": store.promptHeight ? `${store.promptHeight}px` : undefined,
}}
>
@@ -1967,6 +1940,7 @@ export default function Page() {
"sticky top-0 z-30 bg-background-stronger": true,
"w-full": true,
"px-4 md:px-6": true,
"md:max-w-200 md:mx-auto": centered(),
}}
>
<div class="h-10 flex items-center gap-1">
@@ -1991,7 +1965,13 @@ export default function Page() {
<div
ref={autoScroll.contentRef}
role="log"
class="flex flex-col gap-32 items-start justify-start w-full mt-0 pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]"
class="flex flex-col gap-32 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]"
classList={{
"w-full": true,
"md:max-w-200 md:mx-auto": centered(),
"mt-0.5": centered(),
"mt-0": !centered(),
}}
>
<Show when={store.turnStart > 0}>
<div class="w-full flex justify-center">
@@ -2039,7 +2019,10 @@ export default function Page() {
<div
id={anchor(message.id)}
data-message-id={message.id}
class="min-w-0 w-full max-w-full"
classList={{
"min-w-0 w-full max-w-full": true,
"md:max-w-200": centered(),
}}
>
<SessionTurn
sessionID={params.id!}
@@ -2092,7 +2075,12 @@ export default function Page() {
ref={(el) => (promptDock = el)}
class="absolute inset-x-0 bottom-0 pt-12 pb-4 flex flex-col justify-center items-center z-50 px-4 md:px-0 bg-gradient-to-t from-background-stronger via-background-stronger to-transparent pointer-events-none"
>
<div class="w-full px-4 pointer-events-auto">
<div
classList={{
"w-full px-4 pointer-events-auto": true,
"md:max-w-200 md:mx-auto": centered(),
}}
>
<Show when={request()} keyed>
{(perm) => (
<div data-component="tool-part-wrapper" data-permission="true" class="mb-3">
@@ -2163,7 +2151,7 @@ export default function Page() {
</div>
</div>
<Show when={isDesktop()}>
<Show when={isDesktop() && layout.fileTree.opened()}>
<ResizeHandle
direction="horizontal"
size={layout.session.width()}
@@ -2175,7 +2163,7 @@ export default function Page() {
</div>
{/* Desktop side panel - hidden on mobile */}
<Show when={isDesktop()}>
<Show when={isDesktop() && layout.fileTree.opened()}>
<aside
id="review-panel"
aria-label={language.t("session.panel.reviewAndFiles")}
@@ -2183,7 +2171,7 @@ export default function Page() {
>
<div class="flex-1 min-w-0 h-full">
<Show
when={layout.fileTree.opened() && fileTreeTab() === "changes"}
when={fileTreeTab() === "changes"}
fallback={
<DragDropProvider
onDragStart={handleDragStart}
@@ -2195,24 +2183,62 @@ export default function Page() {
<ConstrainDragYAxis />
<Tabs value={activeTab()} onChange={openTab}>
<div class="sticky top-0 shrink-0 flex">
<Tabs.List>
<Show when={reviewTab()}>
<Tabs.Trigger value="review" classes={{ button: "!pl-6" }}>
<div class="flex items-center gap-3">
<Show when={hasReview() && diffsReady()}>
<DiffChanges changes={diffs()} variant="bars" />
</Show>
<div class="flex items-center gap-1.5">
<div>{language.t("session.tab.review")}</div>
<Show when={hasReview()}>
<div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
{reviewCount()}
</div>
</Show>
</div>
</div>
</Tabs.Trigger>
</Show>
<Tabs.List
ref={(el: HTMLDivElement) => {
let scrollTimeout: number | undefined
let prevScrollWidth = el.scrollWidth
let prevContextOpen = contextOpen()
const handler = () => {
if (scrollTimeout !== undefined) clearTimeout(scrollTimeout)
scrollTimeout = window.setTimeout(() => {
const scrollWidth = el.scrollWidth
const clientWidth = el.clientWidth
const currentContextOpen = contextOpen()
// Only scroll when a tab is added (width increased), not on removal
if (scrollWidth > prevScrollWidth) {
if (!prevContextOpen && currentContextOpen) {
// Context tab was opened, scroll to first
el.scrollTo({
left: 0,
behavior: "smooth",
})
} else if (scrollWidth > clientWidth) {
// File tab was added, scroll to rightmost
el.scrollTo({
left: scrollWidth - clientWidth,
behavior: "smooth",
})
}
}
// When width decreases (tab removed), don't scroll - let browser handle it naturally
prevScrollWidth = scrollWidth
prevContextOpen = currentContextOpen
}, 0)
}
const wheelHandler = (e: WheelEvent) => {
// Enable horizontal scrolling with mouse wheel
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
el.scrollLeft += e.deltaY > 0 ? 50 : -50
e.preventDefault()
}
}
el.addEventListener("wheel", wheelHandler, { passive: false })
const observer = new MutationObserver(handler)
observer.observe(el, { childList: true })
onCleanup(() => {
el.removeEventListener("wheel", wheelHandler)
observer.disconnect()
if (scrollTimeout !== undefined) clearTimeout(scrollTimeout)
})
}}
>
<Show when={contextOpen()}>
<Tabs.Trigger
value="context"
@@ -2261,12 +2287,6 @@ export default function Page() {
</Tabs.List>
</div>
<Show when={reviewTab()}>
<Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={activeTab() === "review"}>{reviewPanel()}</Show>
</Tabs.Content>
</Show>
<Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={activeTab() === "empty"}>
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.1.37",
"version": "0.0.0-ci-202601291718",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.1.37",
"version": "0.0.0-ci-202601291718",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.1.37",
"version": "0.0.0-ci-202601291718",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.1.37",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.1.37",
"version": "0.0.0-ci-202601291718",
"type": "module",
"license": "MIT",
"scripts": {
@@ -18,6 +18,7 @@
"@solid-primitives/i18n": "2.2.1",
"@solid-primitives/storage": "catalog:",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-deep-link": "~2",
"@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-os": "~2",
+102 -4
View File
@@ -609,6 +609,26 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "const-random"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
dependencies = [
"const-random-macro",
]
[[package]]
name = "const-random-macro"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
dependencies = [
"getrandom 0.2.16",
"once_cell",
"tiny-keccak",
]
[[package]]
name = "convert_case"
version = "0.4.0"
@@ -980,6 +1000,15 @@ dependencies = [
"rand 0.8.5",
]
[[package]]
name = "dlv-list"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
dependencies = [
"const-random",
]
[[package]]
name = "document-features"
version = "0.2.12"
@@ -1777,6 +1806,12 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hashbrown"
version = "0.15.5"
@@ -1930,7 +1965,7 @@ dependencies = [
"tokio",
"tower-service",
"tracing",
"windows-registry",
"windows-registry 0.6.1",
]
[[package]]
@@ -2345,7 +2380,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4f8240c33bb08c5d8b8cdea87b683b05e61037aa76ff26bef40672cc6ecbb80"
dependencies = [
"freedesktop_entry_parser",
"rust-ini",
"rust-ini 0.17.0",
]
[[package]]
@@ -3038,6 +3073,7 @@ dependencies = [
"tauri-build",
"tauri-plugin-clipboard-manager",
"tauri-plugin-decorum",
"tauri-plugin-deep-link",
"tauri-plugin-dialog",
"tauri-plugin-http",
"tauri-plugin-notification",
@@ -3067,10 +3103,20 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c672c7ad9ec066e428c00eb917124a06f08db19e2584de982cc34b1f4c12485"
dependencies = [
"dlv-list",
"dlv-list 0.2.3",
"hashbrown 0.9.1",
]
[[package]]
name = "ordered-multimap"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
dependencies = [
"dlv-list 0.5.2",
"hashbrown 0.14.5",
]
[[package]]
name = "ordered-stream"
version = "0.2.0"
@@ -3947,7 +3993,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63471c4aa97a1cf8332a5f97709a79a4234698de6a1f5087faf66f2dae810e22"
dependencies = [
"cfg-if",
"ordered-multimap",
"ordered-multimap 0.3.1",
]
[[package]]
name = "rust-ini"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7"
dependencies = [
"cfg-if",
"ordered-multimap 0.7.3",
]
[[package]]
@@ -4817,6 +4873,27 @@ dependencies = [
"tauri-plugin",
]
[[package]]
name = "tauri-plugin-deep-link"
version = "2.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "444b091f24f2f6bdb4a305b54d3961f629c11861c685aceeea9a1972f89e43d5"
dependencies = [
"dunce",
"plist",
"rust-ini 0.21.3",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.17",
"tracing",
"url",
"windows-registry 0.5.3",
"windows-result 0.3.4",
]
[[package]]
name = "tauri-plugin-dialog"
version = "2.4.2"
@@ -4980,6 +5057,7 @@ dependencies = [
"serde",
"serde_json",
"tauri",
"tauri-plugin-deep-link",
"thiserror 2.0.17",
"tracing",
"windows-sys 0.60.2",
@@ -5271,6 +5349,15 @@ dependencies = [
"time-core",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]]
name = "tinystr"
version = "0.8.2"
@@ -6208,6 +6295,17 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-registry"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
dependencies = [
"windows-link 0.1.3",
"windows-result 0.3.4",
"windows-strings 0.4.2",
]
[[package]]
name = "windows-registry"
version = "0.6.1"
+2 -1
View File
@@ -20,6 +20,7 @@ tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = ["macos-private-api", "devtools"] }
tauri-plugin-opener = "2"
tauri-plugin-deep-link = "2.4.6"
tauri-plugin-shell = "2"
tauri-plugin-dialog = "2"
tauri-plugin-updater = "2"
@@ -29,7 +30,7 @@ tauri-plugin-window-state = "2"
tauri-plugin-clipboard-manager = "2"
tauri-plugin-http = "2"
tauri-plugin-notification = "2"
tauri-plugin-single-instance = "2"
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
@@ -6,6 +6,7 @@
"permissions": [
"core:default",
"opener:default",
"deep-link:default",
"core:window:allow-start-dragging",
"core:window:allow-set-theme",
"core:webview:allow-set-webview-zoom",
+6
View File
@@ -16,6 +16,8 @@ use std::{
time::{Duration, Instant},
};
use tauri::{AppHandle, LogicalSize, Manager, RunEvent, State, WebviewWindowBuilder};
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
use tauri_plugin_deep_link::DeepLinkExt;
#[cfg(windows)]
use tauri_plugin_decorum::WebviewWindowExt;
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
@@ -263,6 +265,7 @@ pub fn run() {
let _ = window.unminimize();
}
}))
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_os::init())
.plugin(
tauri_plugin_window_state::Builder::new()
@@ -291,6 +294,9 @@ pub fn run() {
markdown::parse_markdown_command
])
.setup(move |app| {
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
app.deep_link().register_all().ok();
let app = app.handle().clone();
// Initialize log state
@@ -52,5 +52,12 @@
"sidebarImage": "assets/nsis-sidebar.bmp"
}
}
},
"plugins": {
"deep-link": {
"desktop": {
"schemes": ["opencode"]
}
}
}
}
+18
View File
@@ -3,6 +3,7 @@ import "./webview-zoom"
import { render } from "solid-js/web"
import { AppBaseProviders, AppInterface, PlatformProvider, Platform } from "@opencode-ai/app"
import { open, save } from "@tauri-apps/plugin-dialog"
import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link"
import { open as shellOpen } from "@tauri-apps/plugin-shell"
import { type as ostype } from "@tauri-apps/plugin-os"
import { check, Update } from "@tauri-apps/plugin-updater"
@@ -42,6 +43,22 @@ window.getComputedStyle = ((elt: Element, pseudoElt?: string | null) => {
let update: Update | null = null
const deepLinkEvent = "opencode:deep-link"
const emitDeepLinks = (urls: string[]) => {
if (urls.length === 0) return
window.__OPENCODE__ ??= {}
const pending = window.__OPENCODE__.deepLinks ?? []
window.__OPENCODE__.deepLinks = [...pending, ...urls]
window.dispatchEvent(new CustomEvent(deepLinkEvent, { detail: { urls } }))
}
const listenForDeepLinks = async () => {
const startUrls = await getCurrent().catch(() => null)
if (startUrls?.length) emitDeepLinks(startUrls)
await onOpenUrl((urls) => emitDeepLinks(urls)).catch(() => undefined)
}
const createPlatform = (password: Accessor<string | null>): Platform => ({
platform: "desktop",
os: (() => {
@@ -332,6 +349,7 @@ const createPlatform = (password: Accessor<string | null>): Platform => ({
})
createMenu()
void listenForDeepLinks()
render(() => {
const [serverPassword, setServerPassword] = createSignal<string | null>(null)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.1.37",
"version": "0.0.0-ci-202601291718",
"private": true,
"type": "module",
"license": "MIT",
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.1.37"
version = "0.0.0-ci-202601291718"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/anomalyco/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.37/opencode-darwin-arm64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v0.0.0-ci-202601291718/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.37/opencode-darwin-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v0.0.0-ci-202601291718/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.37/opencode-linux-arm64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v0.0.0-ci-202601291718/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.37/opencode-linux-x64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v0.0.0-ci-202601291718/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.37/opencode-windows-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v0.0.0-ci-202601291718/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.1.37",
"version": "0.0.0-ci-202601291718",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.1.37",
"version": "0.0.0-ci-202601291718",
"name": "opencode",
"type": "module",
"license": "MIT",
+11
View File
@@ -179,4 +179,15 @@ for (const item of targets) {
binaries[name] = Script.version
}
if (Script.release) {
for (const key of Object.keys(binaries)) {
if (key.includes("linux")) {
await $`tar -czf ../../${key}.tar.gz *`.cwd(`dist/${key}/bin`)
} else {
await $`zip -r ../../${key}.zip *`.cwd(`dist/${key}/bin`)
}
}
await $`gh release upload v${Script.version} ./dist/*.zip ./dist/*.tar.gz --clobber`
}
export { binaries }
@@ -1,187 +0,0 @@
#!/usr/bin/env bun
import { $ } from "bun"
import { Script } from "@opencode-ai/script"
if (!Script.preview) {
// Calculate SHA values
const arm64Sha = await $`sha256sum ./dist/opencode-linux-arm64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim())
const x64Sha = await $`sha256sum ./dist/opencode-linux-x64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim())
const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const [pkgver, _subver = ""] = Script.version.split(/(-.*)/, 2)
// arch
const binaryPkgbuild = [
"# Maintainer: dax",
"# Maintainer: adam",
"",
"pkgname='opencode-bin'",
`pkgver=${pkgver}`,
`_subver=${_subver}`,
"options=('!debug' '!strip')",
"pkgrel=1",
"pkgdesc='The AI coding agent built for the terminal.'",
"url='https://github.com/anomalyco/opencode'",
"arch=('aarch64' 'x86_64')",
"license=('MIT')",
"provides=('opencode')",
"conflicts=('opencode')",
"depends=('ripgrep')",
"",
`source_aarch64=("\${pkgname}_\${pkgver}_aarch64.tar.gz::https://github.com/anomalyco/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-arm64.tar.gz")`,
`sha256sums_aarch64=('${arm64Sha}')`,
`source_x86_64=("\${pkgname}_\${pkgver}_x86_64.tar.gz::https://github.com/anomalyco/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-x64.tar.gz")`,
`sha256sums_x86_64=('${x64Sha}')`,
"",
"package() {",
' install -Dm755 ./opencode "${pkgdir}/usr/bin/opencode"',
"}",
"",
].join("\n")
// Source-based PKGBUILD for opencode
const sourcePkgbuild = [
"# Maintainer: dax",
"# Maintainer: adam",
"",
"pkgname='opencode'",
`pkgver=${pkgver}`,
`_subver=${_subver}`,
"options=('!debug' '!strip')",
"pkgrel=1",
"pkgdesc='The AI coding agent built for the terminal.'",
"url='https://github.com/anomalyco/opencode'",
"arch=('aarch64' 'x86_64')",
"license=('MIT')",
"provides=('opencode')",
"conflicts=('opencode-bin')",
"depends=('ripgrep')",
"makedepends=('git' 'bun' 'go')",
"",
`source=("opencode-\${pkgver}.tar.gz::https://github.com/anomalyco/opencode/archive/v\${pkgver}\${_subver}.tar.gz")`,
`sha256sums=('SKIP')`,
"",
"build() {",
` cd "opencode-\${pkgver}"`,
` bun install`,
" cd ./packages/opencode",
` OPENCODE_CHANNEL=latest OPENCODE_VERSION=${pkgver} bun run ./script/build.ts --single`,
"}",
"",
"package() {",
` cd "opencode-\${pkgver}/packages/opencode"`,
' mkdir -p "${pkgdir}/usr/bin"',
' target_arch="x64"',
' case "$CARCH" in',
' x86_64) target_arch="x64" ;;',
' aarch64) target_arch="arm64" ;;',
' *) printf "unsupported architecture: %s\\n" "$CARCH" >&2 ; return 1 ;;',
" esac",
' libc=""',
" if command -v ldd >/dev/null 2>&1; then",
" if ldd --version 2>&1 | grep -qi musl; then",
' libc="-musl"',
" fi",
" fi",
' if [ -z "$libc" ] && ls /lib/ld-musl-* >/dev/null 2>&1; then',
' libc="-musl"',
" fi",
' base=""',
' if [ "$target_arch" = "x64" ]; then',
" if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then",
' base="-baseline"',
" fi",
" fi",
' bin="dist/opencode-linux-${target_arch}${base}${libc}/bin/opencode"',
' if [ ! -f "$bin" ]; then',
' printf "unable to find binary for %s%s%s\\n" "$target_arch" "$base" "$libc" >&2',
" return 1",
" fi",
' install -Dm755 "$bin" "${pkgdir}/usr/bin/opencode"',
"}",
"",
].join("\n")
for (const [pkg, pkgbuild] of [
["opencode-bin", binaryPkgbuild],
["opencode", sourcePkgbuild],
]) {
for (let i = 0; i < 30; i++) {
try {
await $`rm -rf ./dist/aur-${pkg}`
await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}`
await $`cd ./dist/aur-${pkg} && git checkout master`
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild)
await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO`
await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${Script.version}"`
await $`cd ./dist/aur-${pkg} && git push`
break
} catch (e) {
continue
}
}
}
// Homebrew formula
const homebrewFormula = [
"# typed: false",
"# frozen_string_literal: true",
"",
"# This file was generated by GoReleaser. DO NOT EDIT.",
"class Opencode < Formula",
` desc "The AI coding agent built for the terminal."`,
` homepage "https://github.com/anomalyco/opencode"`,
` version "${Script.version.split("-")[0]}"`,
"",
` depends_on "ripgrep"`,
"",
" on_macos do",
" if Hardware::CPU.intel?",
` url "https://github.com/anomalyco/opencode/releases/download/v${Script.version}/opencode-darwin-x64.zip"`,
` sha256 "${macX64Sha}"`,
"",
" def install",
' bin.install "opencode"',
" end",
" end",
" if Hardware::CPU.arm?",
` url "https://github.com/anomalyco/opencode/releases/download/v${Script.version}/opencode-darwin-arm64.zip"`,
` sha256 "${macArm64Sha}"`,
"",
" def install",
' bin.install "opencode"',
" end",
" end",
" end",
"",
" on_linux do",
" if Hardware::CPU.intel? and Hardware::CPU.is_64_bit?",
` url "https://github.com/anomalyco/opencode/releases/download/v${Script.version}/opencode-linux-x64.tar.gz"`,
` sha256 "${x64Sha}"`,
" def install",
' bin.install "opencode"',
" end",
" end",
" if Hardware::CPU.arm? and Hardware::CPU.is_64_bit?",
` url "https://github.com/anomalyco/opencode/releases/download/v${Script.version}/opencode-linux-arm64.tar.gz"`,
` sha256 "${arm64Sha}"`,
" def install",
' bin.install "opencode"',
" end",
" end",
" end",
"end",
"",
"",
].join("\n")
await $`rm -rf ./dist/homebrew-tap`
await $`git clone https://${process.env["GITHUB_TOKEN"]}@github.com/sst/homebrew-tap.git ./dist/homebrew-tap`
await Bun.file("./dist/homebrew-tap/opencode.rb").write(homebrewFormula)
await $`cd ./dist/homebrew-tap && git add opencode.rb`
await $`cd ./dist/homebrew-tap && git commit -m "Update to v${Script.version}"`
await $`cd ./dist/homebrew-tap && git push`
}
+194 -25
View File
@@ -7,12 +7,13 @@ import { fileURLToPath } from "url"
const dir = fileURLToPath(new URL("..", import.meta.url))
process.chdir(dir)
const { binaries } = await import("./build.ts")
{
const name = `${pkg.name}-${process.platform}-${process.arch}`
console.log(`smoke test: running dist/${name}/bin/opencode --version`)
await $`./dist/${name}/bin/opencode --version`
const binaries: Record<string, string> = {}
for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: "./dist" })) {
const pkg = await Bun.file(`./dist/${filepath}`).json()
binaries[pkg.name] = pkg.version
}
console.log("binaries", binaries)
const version = Object.values(binaries)[0]
await $`mkdir -p ./dist/${pkg.name}`
await $`cp -r ./bin ./dist/${pkg.name}/bin`
@@ -28,7 +29,7 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
scripts: {
postinstall: "bun ./postinstall.mjs || node ./postinstall.mjs",
},
version: Script.version,
version: version,
optionalDependencies: binaries,
},
null,
@@ -36,35 +37,203 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
),
)
const tags = [Script.channel]
const tasks = Object.entries(binaries).map(async ([name]) => {
if (process.platform !== "win32") {
await $`chmod -R 755 .`.cwd(`./dist/${name}`)
}
await $`bun pm pack`.cwd(`./dist/${name}`)
for (const tag of tags) {
await $`npm publish *.tgz --access public --tag ${tag}`.cwd(`./dist/${name}`)
}
await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(`./dist/${name}`)
})
await Promise.all(tasks)
for (const tag of tags) {
await $`cd ./dist/${pkg.name} && bun pm pack && npm publish *.tgz --access public --tag ${tag}`
}
await $`cd ./dist/${pkg.name} && bun pm pack && npm publish *.tgz --access public --tag ${Script.channel}`
const image = "ghcr.io/anomalyco/opencode"
const platforms = "linux/amd64,linux/arm64"
const tags = [`${image}:${version}`, `${image}:${Script.channel}`]
const tagFlags = tags.flatMap((t) => ["-t", t])
await $`docker buildx build --platform ${platforms} ${tagFlags} --push .`
// registries
if (!Script.preview) {
// Create archives for GitHub release
for (const key of Object.keys(binaries)) {
if (key.includes("linux")) {
await $`tar -czf ../../${key}.tar.gz *`.cwd(`dist/${key}/bin`)
} else {
await $`zip -r ../../${key}.zip *`.cwd(`dist/${key}/bin`)
// Calculate SHA values
const arm64Sha = await $`sha256sum ./dist/opencode-linux-arm64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim())
const x64Sha = await $`sha256sum ./dist/opencode-linux-x64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim())
const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const [pkgver, _subver = ""] = Script.version.split(/(-.*)/, 2)
// arch
const binaryPkgbuild = [
"# Maintainer: dax",
"# Maintainer: adam",
"",
"pkgname='opencode-bin'",
`pkgver=${pkgver}`,
`_subver=${_subver}`,
"options=('!debug' '!strip')",
"pkgrel=1",
"pkgdesc='The AI coding agent built for the terminal.'",
"url='https://github.com/anomalyco/opencode'",
"arch=('aarch64' 'x86_64')",
"license=('MIT')",
"provides=('opencode')",
"conflicts=('opencode')",
"depends=('ripgrep')",
"",
`source_aarch64=("\${pkgname}_\${pkgver}_aarch64.tar.gz::https://github.com/anomalyco/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-arm64.tar.gz")`,
`sha256sums_aarch64=('${arm64Sha}')`,
`source_x86_64=("\${pkgname}_\${pkgver}_x86_64.tar.gz::https://github.com/anomalyco/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-x64.tar.gz")`,
`sha256sums_x86_64=('${x64Sha}')`,
"",
"package() {",
' install -Dm755 ./opencode "${pkgdir}/usr/bin/opencode"',
"}",
"",
].join("\n")
// Source-based PKGBUILD for opencode
const sourcePkgbuild = [
"# Maintainer: dax",
"# Maintainer: adam",
"",
"pkgname='opencode'",
`pkgver=${pkgver}`,
`_subver=${_subver}`,
"options=('!debug' '!strip')",
"pkgrel=1",
"pkgdesc='The AI coding agent built for the terminal.'",
"url='https://github.com/anomalyco/opencode'",
"arch=('aarch64' 'x86_64')",
"license=('MIT')",
"provides=('opencode')",
"conflicts=('opencode-bin')",
"depends=('ripgrep')",
"makedepends=('git' 'bun' 'go')",
"",
`source=("opencode-\${pkgver}.tar.gz::https://github.com/anomalyco/opencode/archive/v\${pkgver}\${_subver}.tar.gz")`,
`sha256sums=('SKIP')`,
"",
"build() {",
` cd "opencode-\${pkgver}"`,
` bun install`,
" cd ./packages/opencode",
` OPENCODE_CHANNEL=latest OPENCODE_VERSION=${pkgver} bun run ./script/build.ts --single`,
"}",
"",
"package() {",
` cd "opencode-\${pkgver}/packages/opencode"`,
' mkdir -p "${pkgdir}/usr/bin"',
' target_arch="x64"',
' case "$CARCH" in',
' x86_64) target_arch="x64" ;;',
' aarch64) target_arch="arm64" ;;',
' *) printf "unsupported architecture: %s\\n" "$CARCH" >&2 ; return 1 ;;',
" esac",
' libc=""',
" if command -v ldd >/dev/null 2>&1; then",
" if ldd --version 2>&1 | grep -qi musl; then",
' libc="-musl"',
" fi",
" fi",
' if [ -z "$libc" ] && ls /lib/ld-musl-* >/dev/null 2>&1; then',
' libc="-musl"',
" fi",
' base=""',
' if [ "$target_arch" = "x64" ]; then',
" if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then",
' base="-baseline"',
" fi",
" fi",
' bin="dist/opencode-linux-${target_arch}${base}${libc}/bin/opencode"',
' if [ ! -f "$bin" ]; then',
' printf "unable to find binary for %s%s%s\\n" "$target_arch" "$base" "$libc" >&2',
" return 1",
" fi",
' install -Dm755 "$bin" "${pkgdir}/usr/bin/opencode"',
"}",
"",
].join("\n")
for (const [pkg, pkgbuild] of [
["opencode-bin", binaryPkgbuild],
["opencode", sourcePkgbuild],
]) {
for (let i = 0; i < 30; i++) {
try {
await $`rm -rf ./dist/aur-${pkg}`
await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}`
await $`cd ./dist/aur-${pkg} && git checkout master`
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild)
await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO`
await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${Script.version}"`
await $`cd ./dist/aur-${pkg} && git push`
break
} catch (e) {
continue
}
}
}
const image = "ghcr.io/anomalyco/opencode"
const platforms = "linux/amd64,linux/arm64"
const tags = [`${image}:${Script.version}`, `${image}:latest`]
const tagFlags = tags.flatMap((t) => ["-t", t])
await $`docker buildx build --platform ${platforms} ${tagFlags} --push .`
// Homebrew formula
const homebrewFormula = [
"# typed: false",
"# frozen_string_literal: true",
"",
"# This file was generated by GoReleaser. DO NOT EDIT.",
"class Opencode < Formula",
` desc "The AI coding agent built for the terminal."`,
` homepage "https://github.com/anomalyco/opencode"`,
` version "${Script.version.split("-")[0]}"`,
"",
` depends_on "ripgrep"`,
"",
" on_macos do",
" if Hardware::CPU.intel?",
` url "https://github.com/anomalyco/opencode/releases/download/v${Script.version}/opencode-darwin-x64.zip"`,
` sha256 "${macX64Sha}"`,
"",
" def install",
' bin.install "opencode"',
" end",
" end",
" if Hardware::CPU.arm?",
` url "https://github.com/anomalyco/opencode/releases/download/v${Script.version}/opencode-darwin-arm64.zip"`,
` sha256 "${macArm64Sha}"`,
"",
" def install",
' bin.install "opencode"',
" end",
" end",
" end",
"",
" on_linux do",
" if Hardware::CPU.intel? and Hardware::CPU.is_64_bit?",
` url "https://github.com/anomalyco/opencode/releases/download/v${Script.version}/opencode-linux-x64.tar.gz"`,
` sha256 "${x64Sha}"`,
" def install",
' bin.install "opencode"',
" end",
" end",
" if Hardware::CPU.arm? and Hardware::CPU.is_64_bit?",
` url "https://github.com/anomalyco/opencode/releases/download/v${Script.version}/opencode-linux-arm64.tar.gz"`,
` sha256 "${arm64Sha}"`,
" def install",
' bin.install "opencode"',
" end",
" end",
" end",
"end",
"",
"",
].join("\n")
await $`rm -rf ./dist/homebrew-tap`
await $`git clone https://${process.env["GITHUB_TOKEN"]}@github.com/sst/homebrew-tap.git ./dist/homebrew-tap`
await Bun.file("./dist/homebrew-tap/opencode.rb").write(homebrewFormula)
await $`cd ./dist/homebrew-tap && git add opencode.rb`
await $`cd ./dist/homebrew-tap && git commit -m "Update to v${Script.version}"`
await $`cd ./dist/homebrew-tap && git push`
}
+2 -5
View File
@@ -1,6 +1,5 @@
import path from "path"
import { Global } from "../global"
import fs from "fs/promises"
import z from "zod"
export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key"
@@ -59,15 +58,13 @@ export namespace Auth {
export async function set(key: string, info: Info) {
const file = Bun.file(filepath)
const data = await all()
await Bun.write(file, JSON.stringify({ ...data, [key]: info }, null, 2))
await fs.chmod(file.name!, 0o600)
await Bun.write(file, JSON.stringify({ ...data, [key]: info }, null, 2), { mode: 0o600 })
}
export async function remove(key: string) {
const file = Bun.file(filepath)
const data = await all()
delete data[key]
await Bun.write(file, JSON.stringify(data, null, 2))
await fs.chmod(file.name!, 0o600)
await Bun.write(file, JSON.stringify(data, null, 2), { mode: 0o600 })
}
}
+28 -4
View File
@@ -27,6 +27,10 @@ interface SessionStats {
tokens: {
input: number
output: number
cache: {
read: number
write: number
}
}
cost: number
}
@@ -191,6 +195,10 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
tokens: {
input: number
output: number
cache: {
read: number
write: number
}
}
cost: number
}
@@ -204,7 +212,7 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
if (!sessionModelUsage[modelKey]) {
sessionModelUsage[modelKey] = {
messages: 0,
tokens: { input: 0, output: 0 },
tokens: { input: 0, output: 0, cache: { read: 0, write: 0 } },
cost: 0,
}
}
@@ -221,6 +229,8 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
sessionModelUsage[modelKey].tokens.input += message.info.tokens.input || 0
sessionModelUsage[modelKey].tokens.output +=
(message.info.tokens.output || 0) + (message.info.tokens.reasoning || 0)
sessionModelUsage[modelKey].tokens.cache.read += message.info.tokens.cache?.read || 0
sessionModelUsage[modelKey].tokens.cache.write += message.info.tokens.cache?.write || 0
}
}
@@ -235,7 +245,12 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
messageCount: messages.length,
sessionCost,
sessionTokens,
sessionTotalTokens: sessionTokens.input + sessionTokens.output + sessionTokens.reasoning,
sessionTotalTokens:
sessionTokens.input +
sessionTokens.output +
sessionTokens.reasoning +
sessionTokens.cache.read +
sessionTokens.cache.write,
sessionToolUsage,
sessionModelUsage,
earliestTime: cutoffTime > 0 ? session.time.updated : session.time.created,
@@ -266,13 +281,15 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
if (!stats.modelUsage[model]) {
stats.modelUsage[model] = {
messages: 0,
tokens: { input: 0, output: 0 },
tokens: { input: 0, output: 0, cache: { read: 0, write: 0 } },
cost: 0,
}
}
stats.modelUsage[model].messages += usage.messages
stats.modelUsage[model].tokens.input += usage.tokens.input
stats.modelUsage[model].tokens.output += usage.tokens.output
stats.modelUsage[model].tokens.cache.read += usage.tokens.cache.read
stats.modelUsage[model].tokens.cache.write += usage.tokens.cache.write
stats.modelUsage[model].cost += usage.cost
}
}
@@ -286,7 +303,12 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
}
stats.days = effectiveDays
stats.costPerDay = stats.totalCost / effectiveDays
const totalTokens = stats.totalTokens.input + stats.totalTokens.output + stats.totalTokens.reasoning
const totalTokens =
stats.totalTokens.input +
stats.totalTokens.output +
stats.totalTokens.reasoning +
stats.totalTokens.cache.read +
stats.totalTokens.cache.write
stats.tokensPerSession = filteredSessions.length > 0 ? totalTokens / filteredSessions.length : 0
sessionTotalTokens.sort((a, b) => a - b)
const mid = Math.floor(sessionTotalTokens.length / 2)
@@ -353,6 +375,8 @@ export function displayStats(stats: SessionStats, toolLimit?: number, modelLimit
console.log(renderRow(" Messages", usage.messages.toLocaleString()))
console.log(renderRow(" Input Tokens", formatNumber(usage.tokens.input)))
console.log(renderRow(" Output Tokens", formatNumber(usage.tokens.output)))
console.log(renderRow(" Cache Read", formatNumber(usage.tokens.cache.read)))
console.log(renderRow(" Cache Write", formatNumber(usage.tokens.cache.write)))
console.log(renderRow(" Cost", `$${usage.cost.toFixed(4)}`))
console.log("├────────────────────────────────────────────────────────┤")
}
@@ -1,16 +1,13 @@
import { TextAttributes, RGBA } from "@opentui/core"
import { For, type JSX } from "solid-js"
import { useTheme, tint } from "@tui/context/theme"
import { logo, marks } from "@/cli/logo"
// Shadow markers (rendered chars in parens):
// _ = full shadow cell (space with bg=shadow)
// ^ = letter top, shadow bottom (▀ with fg=letter, bg=shadow)
// ~ = shadow top only (▀ with fg=shadow)
const SHADOW_MARKER = /[_^~]/
const LOGO_LEFT = [` `, `█▀▀█ █▀▀█ █▀▀█ █▀▀▄`, `█__█ █__█ █^^^ █__█`, `▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀~~▀`]
const LOGO_RIGHT = [``, `█▀▀▀ █▀▀█ █▀▀█ █▀▀█`, `█___ █__█ █__█ █^^^`, `▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`]
const SHADOW_MARKER = new RegExp(`[${marks}]`)
export function Logo() {
const { theme } = useTheme()
@@ -75,11 +72,11 @@ export function Logo() {
return (
<box>
<For each={LOGO_LEFT}>
<For each={logo.left}>
{(line, index) => (
<box flexDirection="row" gap={1}>
<box flexDirection="row">{renderLine(line, theme.textMuted, false)}</box>
<box flexDirection="row">{renderLine(LOGO_RIGHT[index()], theme.text, true)}</box>
<box flexDirection="row">{renderLine(logo.right[index()], theme.text, true)}</box>
</box>
)}
</For>
@@ -93,8 +93,11 @@ export function Prompt(props: PromptProps) {
let promptPartTypeId = 0
sdk.event.on(TuiEvent.PromptAppend.type, (evt) => {
if (!input || input.isDestroyed) return
input.insertText(evt.properties.text)
setTimeout(() => {
// setTimeout is a workaround and needs to be addressed properly
if (!input || input.isDestroyed) return
input.getLayoutNode().markDirty()
input.gotoBufferEnd()
renderer.requestRender()
@@ -924,6 +927,8 @@ export function Prompt(props: PromptProps) {
// Force layout update and render for the pasted content
setTimeout(() => {
// setTimeout is a workaround and needs to be addressed properly
if (!input || input.isDestroyed) return
input.getLayoutNode().markDirty()
renderer.requestRender()
}, 0)
@@ -935,6 +940,8 @@ export function Prompt(props: PromptProps) {
}
props.ref?.(ref)
setTimeout(() => {
// setTimeout is a workaround and needs to be addressed properly
if (!input || input.isDestroyed) return
input.cursorColor = theme.text
}, 0)
}}
@@ -34,9 +34,8 @@ export const { use: useKeybind, provider: KeybindProvider } = createSimpleContex
timeout = setTimeout(() => {
if (!store.leader) return
leader(false)
if (focus) {
focus.focus()
}
if (!focus || focus.isDestroyed) return
focus.focus()
}, 2000)
return
}
@@ -58,6 +58,7 @@ import { DialogTimeline } from "./dialog-timeline"
import { DialogForkFromTimeline } from "./dialog-fork-from-timeline"
import { DialogSessionRename } from "../../component/dialog-session-rename"
import { Sidebar } from "./sidebar"
import { Flag } from "@/flag/flag"
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
import parsers from "../../../../../../parsers-config.ts"
import { Clipboard } from "../../util/clipboard"
@@ -274,7 +275,8 @@ export function Session() {
function toBottom() {
setTimeout(() => {
if (scroll) scroll.scrollTo(scroll.scrollHeight)
if (!scroll || scroll.isDestroyed) return
scroll.scrollTo(scroll.scrollHeight)
}, 50)
}
@@ -1338,15 +1340,27 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess
return (
<Show when={props.part.text.trim()}>
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0}>
<code
filetype="markdown"
drawUnstyledText={false}
streaming={true}
syntaxStyle={syntax()}
content={props.part.text.trim()}
conceal={ctx.conceal()}
fg={theme.text}
/>
<Switch>
<Match when={Flag.OPENCODE_EXPERIMENTAL_MARKDOWN}>
<markdown
syntaxStyle={syntax()}
streaming={true}
content={props.part.text.trim()}
conceal={ctx.conceal()}
/>
</Match>
<Match when={!Flag.OPENCODE_EXPERIMENTAL_MARKDOWN}>
<code
filetype="markdown"
drawUnstyledText={false}
streaming={true}
syntaxStyle={syntax()}
content={props.part.text.trim()}
conceal={ctx.conceal()}
fg={theme.text}
/>
</Match>
</Switch>
</box>
</Show>
)
@@ -68,6 +68,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
onMount(() => {
dialog.setSize("medium")
setTimeout(() => {
if (!textarea || textarea.isDestroyed) return
textarea.focus()
}, 1)
textarea.gotoLineEnd()
@@ -27,6 +27,7 @@ export function DialogPrompt(props: DialogPromptProps) {
onMount(() => {
dialog.setSize("medium")
setTimeout(() => {
if (!textarea || textarea.isDestroyed) return
textarea.focus()
}, 1)
textarea.gotoLineEnd()
@@ -241,7 +241,11 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
focusedTextColor={theme.textMuted}
ref={(r) => {
input = r
setTimeout(() => input.focus(), 1)
setTimeout(() => {
if (!input) return
if (input.isDestroyed) return
input.focus()
}, 1)
}}
placeholder={props.placeholder ?? "Search"}
/>
+6
View File
@@ -0,0 +1,6 @@
export const logo = {
left: [" ", "█▀▀█ █▀▀█ █▀▀█ █▀▀▄", "█__█ █__█ █^^^ █__█", "▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀~~▀"],
right: [" ▄ ", "█▀▀▀ █▀▀█ █▀▀█ █▀▀█", "█___ █__█ █__█ █^^^", "▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀"],
}
export const marks = "_^~"
+44 -15
View File
@@ -1,15 +1,9 @@
import z from "zod"
import { EOL } from "os"
import { NamedError } from "@opencode-ai/util/error"
import { logo as glyphs } from "./logo"
export namespace UI {
const LOGO = [
[`  `, ``],
[`█▀▀█ █▀▀█ █▀▀█ █▀▀▄ `, `█▀▀▀ █▀▀█ █▀▀█ █▀▀█`],
[`█░░█ █░░█ █▀▀▀ █░░█ `, `█░░░ █░░█ █░░█ █▀▀▀`],
[`▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ `, `▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`],
]
export const CancelledError = NamedError.create("UICancelledError", z.void())
export const Style = {
@@ -47,15 +41,50 @@ export namespace UI {
}
export function logo(pad?: string) {
const result = []
for (const row of LOGO) {
if (pad) result.push(pad)
result.push(Bun.color("gray", "ansi"))
result.push(row[0])
result.push("\x1b[0m")
result.push(row[1])
result.push(EOL)
const result: string[] = []
const reset = "\x1b[0m"
const left = {
fg: Bun.color("gray", "ansi") ?? "",
shadow: "\x1b[38;5;235m",
bg: "\x1b[48;5;235m",
}
const right = {
fg: reset,
shadow: "\x1b[38;5;238m",
bg: "\x1b[48;5;238m",
}
const gap = " "
const draw = (line: string, fg: string, shadow: string, bg: string) => {
const parts: string[] = []
for (const char of line) {
if (char === "_") {
parts.push(bg, " ", reset)
continue
}
if (char === "^") {
parts.push(fg, bg, "▀", reset)
continue
}
if (char === "~") {
parts.push(shadow, "▀", reset)
continue
}
if (char === " ") {
parts.push(" ")
continue
}
parts.push(fg, char, reset)
}
return parts.join("")
}
glyphs.left.forEach((row, index) => {
if (pad) result.push(pad)
result.push(draw(row, left.fg, left.shadow, left.bg))
result.push(gap)
const other = glyphs.right[index] ?? ""
result.push(draw(other, right.fg, right.shadow, right.bg))
result.push(EOL)
})
return result.join("").trimEnd()
}
+50 -29
View File
@@ -540,6 +540,7 @@ export namespace Config {
codesearch: PermissionAction.optional(),
lsp: PermissionRule.optional(),
doom_loop: PermissionAction.optional(),
skill: PermissionRule.optional(),
})
.catchall(PermissionRule)
.or(PermissionAction),
@@ -559,6 +560,11 @@ export namespace Config {
})
export type Command = z.infer<typeof Command>
export const Skills = z.object({
paths: z.array(z.string()).optional().describe("Additional paths to skill folders"),
})
export type Skills = z.infer<typeof Skills>
export const Agent = z
.object({
model: z.string().optional(),
@@ -894,6 +900,7 @@ export namespace Config {
.record(z.string(), Command)
.optional()
.describe("Command configuration, see https://opencode.ai/docs/commands"),
skills: Skills.optional().describe("Additional skill folder paths"),
watcher: z
.object({
ignore: z.array(z.string()).optional(),
@@ -1104,20 +1111,23 @@ export namespace Config {
mergeDeep(await loadFile(path.join(Global.Path.config, "opencode.jsonc"))),
)
await import(path.join(Global.Path.config, "config"), {
with: {
type: "toml",
},
})
.then(async (mod) => {
const { provider, model, ...rest } = mod.default
if (provider && model) result.model = `${provider}/${model}`
result["$schema"] = "https://opencode.ai/config.json"
result = mergeDeep(result, rest)
await Bun.write(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2))
await fs.unlink(path.join(Global.Path.config, "config"))
const legacy = path.join(Global.Path.config, "config")
if (existsSync(legacy)) {
await import(pathToFileURL(legacy).href, {
with: {
type: "toml",
},
})
.catch(() => {})
.then(async (mod) => {
const { provider, model, ...rest } = mod.default
if (provider && model) result.model = `${provider}/${model}`
result["$schema"] = "https://opencode.ai/config.json"
result = mergeDeep(result, rest)
await Bun.write(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2))
await fs.unlink(legacy)
})
.catch(() => {})
}
return result
})
@@ -1341,24 +1351,35 @@ export namespace Config {
throw new JsonError({ path: filepath }, { cause: err })
})
if (!filepath.endsWith(".jsonc")) {
const existing = parseConfig(before, filepath)
await Bun.write(filepath, JSON.stringify(mergeDeep(existing, config), null, 2))
} else {
const next = patchJsonc(before, config)
parseConfig(next, filepath)
await Bun.write(filepath, next)
}
const next = await (async () => {
if (!filepath.endsWith(".jsonc")) {
const existing = parseConfig(before, filepath)
const merged = mergeDeep(existing, config)
await Bun.write(filepath, JSON.stringify(merged, null, 2))
return merged
}
const updated = patchJsonc(before, config)
const merged = parseConfig(updated, filepath)
await Bun.write(filepath, updated)
return merged
})()
global.reset()
await Instance.disposeAll()
GlobalBus.emit("event", {
directory: "global",
payload: {
type: Event.Disposed.type,
properties: {},
},
})
void Instance.disposeAll()
.catch(() => undefined)
.finally(() => {
GlobalBus.emit("event", {
directory: "global",
payload: {
type: Event.Disposed.type,
properties: {},
},
})
})
return next
}
export async function directories() {
+17 -12
View File
@@ -14,7 +14,9 @@ export namespace ConfigMarkdown {
return Array.from(template.matchAll(SHELL_REGEX))
}
export function preprocessFrontmatter(content: string): string {
// other coding agents like claude code allow invalid yaml in their
// frontmatter, we need to fallback to a more permissive parser for those cases
export function fallbackSanitization(content: string): string {
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
if (!match) return content
@@ -53,7 +55,7 @@ export namespace ConfigMarkdown {
// if value contains a colon, convert to block scalar
if (value.includes(":")) {
result.push(`${key}: |`)
result.push(`${key}: |-`)
result.push(` ${value}`)
continue
}
@@ -66,20 +68,23 @@ export namespace ConfigMarkdown {
}
export async function parse(filePath: string) {
const raw = await Bun.file(filePath).text()
const template = preprocessFrontmatter(raw)
const template = await Bun.file(filePath).text()
try {
const md = matter(template)
return md
} catch (err) {
throw new FrontmatterError(
{
path: filePath,
message: `${filePath}: Failed to parse YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`,
},
{ cause: err },
)
} catch {
try {
return matter(fallbackSanitization(template))
} catch (err) {
throw new FrontmatterError(
{
path: filePath,
message: `${filePath}: Failed to parse YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`,
},
{ cause: err },
)
}
}
}
+10 -2
View File
@@ -209,7 +209,10 @@ export namespace Ripgrep {
hidden?: boolean
follow?: boolean
maxDepth?: number
signal?: AbortSignal
}) {
input.signal?.throwIfAborted()
const args = [await filepath(), "--files", "--glob=!.git/*"]
if (input.follow !== false) args.push("--follow")
if (input.hidden !== false) args.push("--hidden")
@@ -235,6 +238,7 @@ export namespace Ripgrep {
stdout: "pipe",
stderr: "ignore",
maxBuffer: 1024 * 1024 * 20,
signal: input.signal,
})
const reader = proc.stdout.getReader()
@@ -243,6 +247,8 @@ export namespace Ripgrep {
try {
while (true) {
input.signal?.throwIfAborted()
const { done, value } = await reader.read()
if (done) break
@@ -261,11 +267,13 @@ export namespace Ripgrep {
reader.releaseLock()
await proc.exited
}
input.signal?.throwIfAborted()
}
export async function tree(input: { cwd: string; limit?: number }) {
export async function tree(input: { cwd: string; limit?: number; signal?: AbortSignal }) {
log.info("tree", input)
const files = await Array.fromAsync(Ripgrep.files({ cwd: input.cwd }))
const files = await Array.fromAsync(Ripgrep.files({ cwd: input.cwd, signal: input.signal }))
interface Node {
path: string[]
children: Node[]
+1 -1
View File
@@ -38,7 +38,6 @@ export namespace Flag {
export const OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT = truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT")
export const OPENCODE_ENABLE_EXA =
truthy("OPENCODE_ENABLE_EXA") || OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_EXA")
export const OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH = number("OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH")
export const OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS = number("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS")
export const OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX = number("OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX")
export const OPENCODE_EXPERIMENTAL_OXFMT = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_OXFMT")
@@ -46,6 +45,7 @@ export namespace Flag {
export const OPENCODE_EXPERIMENTAL_LSP_TOOL = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_LSP_TOOL")
export const OPENCODE_DISABLE_FILETIME_CHECK = truthy("OPENCODE_DISABLE_FILETIME_CHECK")
export const OPENCODE_EXPERIMENTAL_PLAN_MODE = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_PLAN_MODE")
export const OPENCODE_EXPERIMENTAL_MARKDOWN = truthy("OPENCODE_EXPERIMENTAL_MARKDOWN")
export const OPENCODE_MODELS_URL = process.env["OPENCODE_MODELS_URL"]
function number(key: string) {
+1 -1
View File
@@ -33,7 +33,7 @@ await Promise.all([
fs.mkdir(Global.Path.bin, { recursive: true }),
])
const CACHE_VERSION = "19"
const CACHE_VERSION = "21"
const version = await Bun.file(path.join(Global.Path.cache, "version"))
.text()
+2 -5
View File
@@ -1,5 +1,4 @@
import path from "path"
import fs from "fs/promises"
import z from "zod"
import { Global } from "../global"
@@ -65,16 +64,14 @@ export namespace McpAuth {
if (serverUrl) {
entry.serverUrl = serverUrl
}
await Bun.write(file, JSON.stringify({ ...data, [mcpName]: entry }, null, 2))
await fs.chmod(file.name!, 0o600)
await Bun.write(file, JSON.stringify({ ...data, [mcpName]: entry }, null, 2), { mode: 0o600 })
}
export async function remove(mcpName: string): Promise<void> {
const file = Bun.file(filepath)
const data = await all()
delete data[mcpName]
await Bun.write(file, JSON.stringify(data, null, 2))
await fs.chmod(file.name!, 0o600)
await Bun.write(file, JSON.stringify(data, null, 2), { mode: 0o600 })
}
export async function updateTokens(mcpName: string, tokens: Tokens, serverUrl?: string): Promise<void> {
+37 -11
View File
@@ -40,33 +40,37 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
},
}
// TODO: re-enable once messages api has higher rate limits
// TODO: move some of this hacky-ness to models.dev presets once we have better grasp of things here...
const base = baseURL ?? model.api.url
const claude = model.id.includes("claude")
const url = iife(() => {
if (!claude) return base
if (base.endsWith("/v1")) return base
if (base.endsWith("/")) return `${base}v1`
return `${base}/v1`
})
// const base = baseURL ?? model.api.url
// const claude = model.id.includes("claude")
// const url = iife(() => {
// if (!claude) return base
// if (base.endsWith("/v1")) return base
// if (base.endsWith("/")) return `${base}v1`
// return `${base}/v1`
// })
model.api.url = url
model.api.npm = claude ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot"
// model.api.url = url
// model.api.npm = claude ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot"
model.api.npm = "@ai-sdk/github-copilot"
}
}
return {
baseURL,
apiKey: "",
async fetch(request: RequestInfo | URL, init?: RequestInit) {
const info = await getAuth()
if (info.type !== "oauth") return fetch(request, init)
const url = request instanceof URL ? request.href : request.toString()
const { isVision, isAgent } = iife(() => {
try {
const body = typeof init?.body === "string" ? JSON.parse(init.body) : init?.body
// Completions API
if (body?.messages) {
if (body?.messages && url.includes("completions")) {
const last = body.messages[body.messages.length - 1]
return {
isVision: body.messages.some(
@@ -88,6 +92,28 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
isAgent: last?.role !== "user",
}
}
// Messages API
if (body?.messages) {
const last = body.messages[body.messages.length - 1]
const hasNonToolCalls =
Array.isArray(last?.content) && last.content.some((part: any) => part?.type !== "tool_result")
return {
isVision: body.messages.some(
(item: any) =>
Array.isArray(item?.content) &&
item.content.some(
(part: any) =>
part?.type === "image" ||
// images can be nested inside tool_result content
(part?.type === "tool_result" &&
Array.isArray(part?.content) &&
part.content.some((nested: any) => nested?.type === "image")),
),
),
isAgent: !(last?.role === "user" && hasNonToolCalls),
}
}
} catch {}
return { isVision: false, isAgent: false }
})
+1 -1
View File
@@ -15,7 +15,7 @@ import { CopilotAuthPlugin } from "./copilot"
export namespace Plugin {
const log = Log.create({ service: "plugin" })
const BUILTIN = ["opencode-anthropic-auth@0.0.10", "@gitlab/opencode-gitlab-auth@1.3.2"]
const BUILTIN = ["opencode-anthropic-auth@0.0.13", "@gitlab/opencode-gitlab-auth@1.3.2"]
// Built-in plugins that are directly imported (not installed from npm)
const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin]
+30 -7
View File
@@ -14,6 +14,10 @@ interface Context {
const context = Context.create<Context>("instance")
const cache = new Map<string, Promise<Context>>()
const disposal = {
all: undefined as Promise<void> | undefined,
}
export const Instance = {
async provide<R>(input: { directory: string; init?: () => Promise<any>; fn: () => R }): Promise<R> {
let existing = cache.get(input.directory)
@@ -77,15 +81,34 @@ export const Instance = {
})
},
async disposeAll() {
Log.Default.info("disposing all instances")
for (const [_key, value] of cache) {
const awaited = await value.catch(() => {})
if (awaited) {
await context.provide(await value, async () => {
if (disposal.all) return disposal.all
disposal.all = iife(async () => {
Log.Default.info("disposing all instances")
const entries = [...cache.entries()]
for (const [key, value] of entries) {
if (cache.get(key) !== value) continue
const ctx = await value.catch((error) => {
Log.Default.warn("instance dispose failed", { key, error })
return undefined
})
if (!ctx) {
if (cache.get(key) === value) cache.delete(key)
continue
}
if (cache.get(key) !== value) continue
await context.provide(ctx, async () => {
await Instance.dispose()
})
}
}
cache.clear()
}).finally(() => {
disposal.all = undefined
})
return disposal.all
},
}
+7 -3
View File
@@ -46,20 +46,24 @@ export namespace State {
}, 10000).unref()
const tasks: Promise<void>[] = []
for (const entry of entries.values()) {
for (const [init, entry] of entries) {
if (!entry.dispose) continue
const label = typeof init === "function" ? init.name : String(init)
const task = Promise.resolve(entry.state)
.then((state) => entry.dispose!(state))
.catch((error) => {
log.error("Error while disposing state:", { error, key })
log.error("Error while disposing state:", { error, key, init: label })
})
tasks.push(task)
}
await Promise.all(tasks)
entries.clear()
recordsByKey.delete(key)
await Promise.all(tasks)
disposalFinished = true
log.info("state disposal completed", { key })
}
+1 -1
View File
@@ -977,7 +977,7 @@ export namespace Provider {
...model.headers,
}
const key = Bun.hash.xxHash32(JSON.stringify({ npm: model.api.npm, options }))
const key = Bun.hash.xxHash32(JSON.stringify({ providerID: model.providerID, npm: model.api.npm, options }))
const existing = s.sdk.get(key)
if (existing) return existing
+13 -6
View File
@@ -284,8 +284,8 @@ export namespace ProviderTransform {
if (id.includes("glm-4.7")) return 1.0
if (id.includes("minimax-m2")) return 1.0
if (id.includes("kimi-k2")) {
// kimi-k2-thinking & kimi-k2.5
if (id.includes("thinking") || id.includes("k2.")) {
// kimi-k2-thinking & kimi-k2.5 && kimi-k2p5
if (id.includes("thinking") || id.includes("k2.") || id.includes("k2p")) {
return 1.0
}
return 0.6
@@ -296,7 +296,7 @@ export namespace ProviderTransform {
export function topP(model: Provider.Model) {
const id = model.id.toLowerCase()
if (id.includes("qwen")) return 1
if (id.includes("minimax-m2") || id.includes("kimi-k2.5") || id.includes("gemini")) {
if (id.includes("minimax-m2") || id.includes("kimi-k2.5") || id.includes("kimi-k2p5") || id.includes("gemini")) {
return 0.95
}
return undefined
@@ -319,7 +319,14 @@ export namespace ProviderTransform {
if (!model.capabilities.reasoning) return {}
const id = model.id.toLowerCase()
if (id.includes("deepseek") || id.includes("minimax") || id.includes("glm") || id.includes("mistral")) return {}
if (
id.includes("deepseek") ||
id.includes("minimax") ||
id.includes("glm") ||
id.includes("mistral") ||
id.includes("kimi")
)
return {}
// see: https://docs.x.ai/docs/guides/reasoning#control-how-hard-the-model-thinks
if (id.includes("grok") && id.includes("grok-3-mini")) {
@@ -428,13 +435,13 @@ export namespace ProviderTransform {
high: {
thinking: {
type: "enabled",
budgetTokens: 16000,
budgetTokens: Math.min(16_000, Math.floor(model.limit.output / 2 - 1)),
},
},
max: {
thinking: {
type: "enabled",
budgetTokens: 31999,
budgetTokens: Math.min(31_999, model.limit.output - 1),
},
},
}
+49 -1
View File
@@ -1,5 +1,5 @@
import { Hono } from "hono"
import { describeRoute, resolver } from "hono-openapi"
import { describeRoute, resolver, validator } from "hono-openapi"
import { streamSSE } from "hono/streaming"
import z from "zod"
import { BusEvent } from "@/bus/bus-event"
@@ -8,6 +8,8 @@ import { Instance } from "../../project/instance"
import { Installation } from "@/installation"
import { Log } from "../../util/log"
import { lazy } from "../../util/lazy"
import { Config } from "../../config/config"
import { errors } from "../error"
const log = Log.create({ service: "server" })
@@ -103,6 +105,52 @@ export const GlobalRoutes = lazy(() =>
})
},
)
.get(
"/config",
describeRoute({
summary: "Get global configuration",
description: "Retrieve the current global OpenCode configuration settings and preferences.",
operationId: "global.config.get",
responses: {
200: {
description: "Get global config info",
content: {
"application/json": {
schema: resolver(Config.Info),
},
},
},
},
}),
async (c) => {
return c.json(await Config.getGlobal())
},
)
.patch(
"/config",
describeRoute({
summary: "Update global configuration",
description: "Update global OpenCode configuration settings and preferences.",
operationId: "global.config.update",
responses: {
200: {
description: "Successfully updated global config",
content: {
"application/json": {
schema: resolver(Config.Info),
},
},
},
...errors(400),
},
}),
validator("json", Config.Info),
async (c) => {
const config = c.req.valid("json")
const next = await Config.updateGlobal(config)
return c.json(next)
},
)
.post(
"/dispose",
describeRoute({
+63 -63
View File
@@ -122,6 +122,68 @@ export namespace Server {
}),
)
.route("/global", GlobalRoutes())
.put(
"/auth/:providerID",
describeRoute({
summary: "Set auth credentials",
description: "Set authentication credentials",
operationId: "auth.set",
responses: {
200: {
description: "Successfully set authentication credentials",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(400),
},
}),
validator(
"param",
z.object({
providerID: z.string(),
}),
),
validator("json", Auth.Info),
async (c) => {
const providerID = c.req.valid("param").providerID
const info = c.req.valid("json")
await Auth.set(providerID, info)
return c.json(true)
},
)
.delete(
"/auth/:providerID",
describeRoute({
summary: "Remove auth credentials",
description: "Remove authentication credentials",
operationId: "auth.remove",
responses: {
200: {
description: "Successfully removed authentication credentials",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(400),
},
}),
validator(
"param",
z.object({
providerID: z.string(),
}),
),
async (c) => {
const providerID = c.req.valid("param").providerID
await Auth.remove(providerID)
return c.json(true)
},
)
.use(async (c, next) => {
let directory = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
try {
@@ -409,68 +471,6 @@ export namespace Server {
return c.json(await Format.status())
},
)
.put(
"/auth/:providerID",
describeRoute({
summary: "Set auth credentials",
description: "Set authentication credentials",
operationId: "auth.set",
responses: {
200: {
description: "Successfully set authentication credentials",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(400),
},
}),
validator(
"param",
z.object({
providerID: z.string(),
}),
),
validator("json", Auth.Info),
async (c) => {
const providerID = c.req.valid("param").providerID
const info = c.req.valid("json")
await Auth.set(providerID, info)
return c.json(true)
},
)
.delete(
"/auth/:providerID",
describeRoute({
summary: "Remove auth credentials",
description: "Remove authentication credentials",
operationId: "auth.remove",
responses: {
200: {
description: "Successfully removed authentication credentials",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(400),
},
}),
validator(
"param",
z.object({
providerID: z.string(),
}),
),
async (c) => {
const providerID = c.req.valid("param").providerID
await Auth.remove(providerID)
return c.json(true)
},
)
.get(
"/event",
describeRoute({
@@ -539,7 +539,7 @@ export namespace Server {
})
response.headers.set(
"Content-Security-Policy",
"default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' data:",
"default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:",
)
return response
}) as unknown as Hono,

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