Compare commits

..
52 Commits
Author SHA1 Message Date
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
Aiden Clineandaptdnfapt 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 Emory 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 Oldenburg 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
ideallove 870c38a6aa fix: maxOutputTokens was accidentally hardcoded to undefined (#10995) 2026-01-28 21:28:15 -06:00
Saba TchikhinashviliandClaude Opus 4.5 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
tan 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 Griffin a7d7f5bb07 fix(app): make settings more responsive for mobile and small web/desktop (#10775) 2026-01-28 21:20:34 -06:00
Babou 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 Yaroshuk 0c8de47f7d fix(app): file tabs - auto scroll on open & scroll via mouse wheel (#11070) 2026-01-28 19:45:12 -06:00
Julian Coy 7ad165fbdc fix(typescript errors): remove duplicate keys causing typescript failures (#11071) 2026-01-28 19:41:50 -06:00
Goni Zahavy e5b33f8a5e fix(opencode): add AbortSignal support to Ripgrep.files() and GlobTool (#10833) 2026-01-28 18:47:09 -06:00
Max Kong 90a7e3d64e fix(ui): improve zh duration display formatting (#10844) 2026-01-28 16:53:37 -06:00
Max Kong 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 Kong 40d5d14304 fix(app): fill missing zh keys to avoid English fallback (#10841) 2026-01-28 16:50:50 -06:00
Alex Yaroshuk 36df0d823a fix(app): alignment and padding in dialogs (#10866) 2026-01-28 16:50:00 -06:00
zerone0x 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 Vogel 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. Rossi 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
RenegadeEandyangtianzhe 26e14ce628 fix: add SubtaskPart with metadata reference (#10990)
Co-authored-by: yangtianzhe <yangtianzhe@corp.netease.com>
2026-01-28 11:11:43 -05:00
Sairaj 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 Phantawong 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 Cline 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
97 changed files with 2819 additions and 841 deletions
+2 -1
View File
@@ -29,7 +29,8 @@
<a href="README.ru.md">Русский</a> | <a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> | <a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</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> </p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) [![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)
+1
View File
@@ -213,3 +213,4 @@
| 2026-01-25 | 6,639,082 (+268,063) | 2,187,853 (+30,983) | 8,826,935 (+299,046) | | 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-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-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) |
+15 -15
View File
@@ -23,7 +23,7 @@
}, },
"packages/app": { "packages/app": {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.1.38", "version": "1.1.42",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@@ -73,7 +73,7 @@
}, },
"packages/console/app": { "packages/console/app": {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.1.38", "version": "1.1.42",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "1.15.2", "@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
@@ -107,7 +107,7 @@
}, },
"packages/console/core": { "packages/console/core": {
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.1.38", "version": "1.1.42",
"dependencies": { "dependencies": {
"@aws-sdk/client-sts": "3.782.0", "@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1", "@jsx-email/render": "1.1.1",
@@ -134,7 +134,7 @@
}, },
"packages/console/function": { "packages/console/function": {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.1.38", "version": "1.1.42",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "2.0.0", "@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2", "@ai-sdk/openai": "2.0.2",
@@ -158,7 +158,7 @@
}, },
"packages/console/mail": { "packages/console/mail": {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.1.38", "version": "1.1.42",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
@@ -182,7 +182,7 @@
}, },
"packages/desktop": { "packages/desktop": {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"version": "1.1.38", "version": "1.1.42",
"dependencies": { "dependencies": {
"@opencode-ai/app": "workspace:*", "@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
@@ -212,7 +212,7 @@
}, },
"packages/enterprise": { "packages/enterprise": {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.1.38", "version": "1.1.42",
"dependencies": { "dependencies": {
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*", "@opencode-ai/util": "workspace:*",
@@ -241,7 +241,7 @@
}, },
"packages/function": { "packages/function": {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.1.38", "version": "1.1.42",
"dependencies": { "dependencies": {
"@octokit/auth-app": "8.0.1", "@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:", "@octokit/rest": "catalog:",
@@ -257,7 +257,7 @@
}, },
"packages/opencode": { "packages/opencode": {
"name": "opencode", "name": "opencode",
"version": "1.1.38", "version": "1.1.42",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -361,7 +361,7 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.1.38", "version": "1.1.42",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"zod": "catalog:", "zod": "catalog:",
@@ -381,7 +381,7 @@
}, },
"packages/sdk/js": { "packages/sdk/js": {
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.1.38", "version": "1.1.42",
"devDependencies": { "devDependencies": {
"@hey-api/openapi-ts": "0.90.10", "@hey-api/openapi-ts": "0.90.10",
"@tsconfig/node22": "catalog:", "@tsconfig/node22": "catalog:",
@@ -392,7 +392,7 @@
}, },
"packages/slack": { "packages/slack": {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.1.38", "version": "1.1.42",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1", "@slack/bolt": "^3.17.1",
@@ -405,7 +405,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.1.38", "version": "1.1.42",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@@ -447,7 +447,7 @@
}, },
"packages/util": { "packages/util": {
"name": "@opencode-ai/util", "name": "@opencode-ai/util",
"version": "1.1.38", "version": "1.1.42",
"dependencies": { "dependencies": {
"zod": "catalog:", "zod": "catalog:",
}, },
@@ -458,7 +458,7 @@
}, },
"packages/web": { "packages/web": {
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"version": "1.1.38", "version": "1.1.42",
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "12.6.3", "@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1", "@astrojs/markdown-remark": "6.3.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.1.38", "version": "1.1.42",
"description": "", "description": "",
"type": "module", "type": "module",
"exports": { "exports": {
@@ -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.previous",
"session.next", "session.next",
"terminal.toggle", "terminal.toggle",
"fileTree.toggle", "review.toggle",
] ]
const limit = 5 const limit = 5
@@ -162,6 +162,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
const value = file.tab(path) const value = file.tab(path)
tabs().open(value) tabs().open(value)
file.load(path) file.load(path)
layout.fileTree.open()
layout.fileTree.setTab("all") layout.fileTree.setTab("all")
props.onOpenFile?.(path) props.onOpenFile?.(path)
} }
@@ -195,7 +196,6 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
: language.t("palette.search.placeholder"), : language.t("palette.search.placeholder"),
autofocus: true, autofocus: true,
hideIcon: true, hideIcon: true,
class: "pl-3 pr-2 !mb-0",
}} }}
emptyMessage={language.t("palette.empty")} emptyMessage={language.t("palette.empty")}
loadingMessage={language.t("common.loading")} loadingMessage={language.t("common.loading")}
@@ -223,7 +223,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
</div> </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"> <div class="flex items-center gap-2 min-w-0">
<span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span> <span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
<Show when={item.description}> <Show when={item.description}>
@@ -187,7 +187,7 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
<Kobalte.Portal> <Kobalte.Portal>
<Kobalte.Content <Kobalte.Content
ref={(el) => setStore("content", el)} 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) => { onEscapeKeyDown={(event) => {
setStore("dismiss", "escape") setStore("dismiss", "escape")
setStore("open", false) setStore("open", false)
@@ -5,9 +5,17 @@ import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list" import { List } from "@opencode-ai/ui/list"
import { Tag } from "@opencode-ai/ui/tag" import { Tag } from "@opencode-ai/ui/tag"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon" 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 { DialogConnectProvider } from "./dialog-connect-provider"
import { useLanguage } from "@/context/language" 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 = () => { export const DialogSelectProvider: Component = () => {
const dialog = useDialog() const dialog = useDialog()
@@ -26,11 +34,13 @@ export const DialogSelectProvider: Component = () => {
key={(x) => x?.id} key={(x) => x?.id}
items={() => { items={() => {
language.locale() language.locale()
return providers.all() return [{ id: CUSTOM_ID, name: "Custom provider" }, ...providers.all()]
}} }}
filterKeys={["id", "name"]} filterKeys={["id", "name"]}
groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())} groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())}
sortBy={(a, b) => { 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)) if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id) return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
return a.name.localeCompare(b.name) return a.name.localeCompare(b.name)
@@ -43,13 +53,20 @@ export const DialogSelectProvider: Component = () => {
}} }}
onSelect={(x) => { onSelect={(x) => {
if (!x) return if (!x) return
if (x.id === CUSTOM_ID) {
dialog.show(() => <DialogCustomProvider back="providers" />)
return
}
dialog.show(() => <DialogConnectProvider provider={x.id} />) dialog.show(() => <DialogConnectProvider provider={x.id} />)
}} }}
> >
{(i) => ( {(i) => (
<div class="px-1.25 w-full flex items-center gap-x-3"> <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> <span>{i.name}</span>
<Show when={i.id === CUSTOM_ID}>
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
</Show>
<Show when={i.id === "opencode"}> <Show when={i.id === "opencode"}>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag> <Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Show> </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)) const wantsReview = item.commentOrigin === "review" || (item.commentOrigin !== "file" && commentInReview(item.path))
if (wantsReview) { if (wantsReview) {
layout.fileTree.open()
layout.fileTree.setTab("changes") layout.fileTree.setTab("changes")
if (!layout.fileTree.opened()) tabs().open("review")
requestAnimationFrame(() => comments.setFocus(focus)) requestAnimationFrame(() => comments.setFocus(focus))
return return
} }
layout.fileTree.open()
layout.fileTree.setTab("all") layout.fileTree.setTab("all")
const tab = files.tab(item.path) const tab = files.tab(item.path)
tabs().open(tab) tabs().open(tab)
@@ -1037,13 +1038,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return return
} }
const ctrl = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey
if (store.popover) { if (store.popover) {
if (event.key === "Tab") { if (event.key === "Tab") {
selectPopoverActive() selectPopoverActive()
event.preventDefault() event.preventDefault()
return 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") { if (store.popover === "at") {
atOnKeyDown(event) atOnKeyDown(event)
event.preventDefault() 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 (ctrl && event.code === "KeyG") {
if (store.popover) { if (store.popover) {
setStore("popover", null) setStore("popover", null)
@@ -57,6 +57,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
const openContext = () => { const openContext = () => {
if (!params.id) return if (!params.id) return
layout.fileTree.open()
layout.fileTree.setTab("all") layout.fileTree.setTab("all")
tabs().open("context") tabs().open("context")
tabs().setActive("context") tabs().setActive("context")
@@ -280,17 +280,14 @@ export function SessionHeader() {
</TooltipKeybind> </TooltipKeybind>
</div> </div>
<div class="hidden md:block shrink-0"> <div class="hidden md:block shrink-0">
<TooltipKeybind <TooltipKeybind title={language.t("command.review.toggle")} keybind={command.keybind("review.toggle")}>
title={language.t("command.fileTree.toggle")}
keybind={command.keybind("fileTree.toggle")}
>
<Button <Button
variant="ghost" variant="ghost"
class="group/file-tree-toggle size-6 p-0" class="group/file-tree-toggle size-6 p-0"
onClick={() => layout.fileTree.toggle()} onClick={() => layout.fileTree.toggle()}
aria-label={language.t("command.fileTree.toggle")} aria-label={language.t("command.review.toggle")}
aria-expanded={layout.fileTree.opened()} 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"> <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon <Icon
@@ -130,7 +130,7 @@ export const SettingsGeneral: Component = () => {
const soundOptions = [...SOUND_OPTIONS] const soundOptions = [...SOUND_OPTIONS]
return ( 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="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"> <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> <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) => { const SettingsRow: Component<SettingsRowProps> = (props) => {
return ( 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="flex flex-col gap-0.5"> <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-14-medium text-text-strong">{props.title}</span>
<span class="text-12-regular text-text-weak">{props.description}</span> <span class="text-12-regular text-text-weak">{props.description}</span>
</div> </div>
@@ -352,7 +352,7 @@ export const SettingsKeybinds: Component = () => {
}) })
return ( 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="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 flex-col gap-4 pt-6 pb-6 max-w-[720px]">
<div class="flex items-center justify-between gap-4"> <div class="flex items-center justify-between gap-4">
@@ -39,7 +39,7 @@ export const SettingsModels: Component = () => {
}) })
return ( 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="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 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> <h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
@@ -99,7 +99,7 @@ export const SettingsModels: Component = () => {
{(item) => { {(item) => {
const key = { providerID: item.provider.id, modelID: item.id } const key = { providerID: item.provider.id, modelID: item.id }
return ( 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"> <div class="min-w-0">
<span class="text-14-regular text-text-strong truncate block">{item.name}</span> <span class="text-14-regular text-text-strong truncate block">{item.name}</span>
</div> </div>
@@ -176,13 +176,13 @@ export const SettingsPermissions: Component = () => {
return ( return (
<div class="flex flex-col h-full overflow-y-auto no-scrollbar"> <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="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> <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> <p class="text-14-regular text-text-weak">{language.t("settings.permissions.description")}</p>
</div> </div>
</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"> <div class="flex flex-col gap-2">
<h3 class="text-14-medium text-text-strong">{language.t("settings.permissions.section.tools")}</h3> <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"> <div class="border border-border-weak-base rounded-lg overflow-hidden">
@@ -217,8 +217,8 @@ interface SettingsRowProps {
const SettingsRow: Component<SettingsRowProps> = (props) => { const SettingsRow: Component<SettingsRowProps> = (props) => {
return ( 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-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"> <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-14-medium text-text-strong">{props.title}</span>
<span class="text-12-regular text-text-weak">{props.description}</span> <span class="text-12-regular text-text-weak">{props.description}</span>
</div> </div>
@@ -3,13 +3,15 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Tag } from "@opencode-ai/ui/tag" import { Tag } from "@opencode-ai/ui/tag"
import { showToast } from "@opencode-ai/ui/toast" 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 { popularProviders, useProviders } from "@/hooks/use-providers"
import { createMemo, type Component, For, Show } from "solid-js" import { createMemo, type Component, For, Show } from "solid-js"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSDK } from "@/context/global-sdk"
import { useGlobalSync } from "@/context/global-sync"
import { DialogConnectProvider } from "./dialog-connect-provider" import { DialogConnectProvider } from "./dialog-connect-provider"
import { DialogSelectProvider } from "./dialog-select-provider" import { DialogSelectProvider } from "./dialog-select-provider"
import { DialogCustomProvider } from "./dialog-custom-provider"
type ProviderSource = "env" | "api" | "config" | "custom" type ProviderSource = "env" | "api" | "config" | "custom"
type ProviderMeta = { source?: ProviderSource } type ProviderMeta = { source?: ProviderSource }
@@ -18,8 +20,14 @@ export const SettingsProviders: Component = () => {
const dialog = useDialog() const dialog = useDialog()
const language = useLanguage() const language = useLanguage()
const globalSDK = useGlobalSDK() const globalSDK = useGlobalSDK()
const globalSync = useGlobalSync()
const providers = useProviders() const providers = useProviders()
const icon = (id: string): IconName => {
if (iconNames.includes(id as IconName)) return id as IconName
return "synthetic"
}
const connected = createMemo(() => { const connected = createMemo(() => {
return providers return providers
.connected() .connected()
@@ -42,14 +50,53 @@ export const SettingsProviders: Component = () => {
const current = source(item) const current = source(item)
if (current === "env") return language.t("settings.providers.tag.environment") if (current === "env") return language.t("settings.providers.tag.environment")
if (current === "api") return language.t("provider.connect.method.apiKey") 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") if (current === "custom") return language.t("settings.providers.tag.custom")
return language.t("settings.providers.tag.other") return language.t("settings.providers.tag.other")
} }
const canDisconnect = (item: unknown) => source(item) !== "env" 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) => { 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 await globalSDK.client.auth
.remove({ providerID }) .remove({ providerID })
.then(async () => { .then(async () => {
@@ -68,7 +115,7 @@ export const SettingsProviders: Component = () => {
} }
return ( 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="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]"> <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> <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()}> <For each={connected()}>
{(item) => ( {(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"> <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> <span class="text-14-medium text-text-strong truncate">{item.name}</span>
<Tag>{type(item)}</Tag> <Tag>{type(item)}</Tag>
</div> </div>
@@ -119,10 +166,10 @@ export const SettingsProviders: Component = () => {
<div class="bg-surface-raised-base px-4 rounded-lg"> <div class="bg-surface-raised-base px-4 rounded-lg">
<For each={popular()}> <For each={popular()}>
{(item) => ( {(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 flex-col min-w-0">
<div class="flex items-center gap-x-3"> <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> <span class="text-14-medium text-text-strong">{item.name}</span>
<Show when={item.id === "opencode"}> <Show when={item.id === "opencode"}>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag> <Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
@@ -177,6 +224,27 @@ export const SettingsProviders: Component = () => {
</div> </div>
)} )}
</For> </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> </div>
<Button <Button
+84 -24
View File
@@ -188,7 +188,74 @@ function createGlobalSync() {
config: {}, config: {},
reload: undefined, 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(() => { createEffect(() => {
if (!projectCacheReady()) return if (!projectCacheReady()) return
@@ -210,14 +277,8 @@ function createGlobalSync() {
createEffect(() => { createEffect(() => {
if (globalStore.reload !== "complete") return if (globalStore.reload !== "complete") return
if (bootstrapQueue.length) {
for (const directory of bootstrapQueue) {
bootstrapInstance(directory)
}
bootstrap()
}
bootstrapQueue = []
setGlobalStore("reload", undefined) setGlobalStore("reload", undefined)
refresh()
}) })
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {} const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
@@ -584,9 +645,8 @@ function createGlobalSync() {
if (directory === "global") { if (directory === "global") {
switch (event?.type) { switch (event?.type) {
case "global.disposed": { case "global.disposed": {
if (globalStore.reload) return refresh()
bootstrap() return
break
} }
case "project.updated": { case "project.updated": {
const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id) const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id)
@@ -647,12 +707,8 @@ function createGlobalSync() {
switch (event.type) { switch (event.type) {
case "server.instance.disposed": { case "server.instance.disposed": {
if (globalStore.reload) { push(directory)
bootstrapQueue.push(directory) return
return
}
bootstrapInstance(directory)
break
} }
case "session.created": { case "session.created": {
const info = event.properties.info const info = event.properties.info
@@ -893,6 +949,10 @@ function createGlobalSync() {
} }
}) })
onCleanup(unsub) onCleanup(unsub)
onCleanup(() => {
if (!timer) return
clearTimeout(timer)
})
async function bootstrap() { async function bootstrap() {
const health = await globalSDK.client.global const health = await globalSDK.client.global
@@ -916,7 +976,7 @@ function createGlobalSync() {
}), }),
), ),
retry(() => retry(() =>
globalSDK.client.config.get().then((x) => { globalSDK.client.global.config.get().then((x) => {
setGlobalStore("config", x.data!) setGlobalStore("config", x.data!)
}), }),
), ),
@@ -999,13 +1059,13 @@ function createGlobalSync() {
}, },
child, child,
bootstrap, bootstrap,
updateConfig: async (config: Config) => { updateConfig: (config: Config) => {
setGlobalStore("reload", "pending") setGlobalStore("reload", "pending")
const response = await globalSDK.client.config.update({ config }) return globalSDK.client.global.config.update({ config }).finally(() => {
setTimeout(() => { setTimeout(() => {
setGlobalStore("reload", "complete") setGlobalStore("reload", "complete")
}, 1000) }, 1000)
return response })
}, },
project: { project: {
loadSessions, 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 ar } from "@/i18n/ar"
import { dict as no } from "@/i18n/no" import { dict as no } from "@/i18n/no"
import { dict as br } from "@/i18n/br" 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 uiEn } from "@opencode-ai/ui/i18n/en"
import { dict as uiZh } from "@opencode-ai/ui/i18n/zh" import { dict as uiZh } from "@opencode-ai/ui/i18n/zh"
import { dict as uiZht } from "@opencode-ai/ui/i18n/zht" 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 uiAr } from "@opencode-ai/ui/i18n/ar"
import { dict as uiNo } from "@opencode-ai/ui/i18n/no" import { dict as uiNo } from "@opencode-ai/ui/i18n/no"
import { dict as uiBr } from "@opencode-ai/ui/i18n/br" 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 RawDictionary = typeof en & typeof uiEn
type Dictionary = i18n.Flatten<RawDictionary> 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 { function detectLocale(): Locale {
if (typeof navigator !== "object") return "en" if (typeof navigator !== "object") return "en"
@@ -65,6 +98,7 @@ function detectLocale(): Locale {
) )
return "no" return "no"
if (language.toLowerCase().startsWith("pt")) return "br" if (language.toLowerCase().startsWith("pt")) return "br"
if (language.toLowerCase().startsWith("th")) return "th"
} }
return "en" return "en"
@@ -94,6 +128,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
if (store.locale === "ar") return "ar" if (store.locale === "ar") return "ar"
if (store.locale === "no") return "no" if (store.locale === "no") return "no"
if (store.locale === "br") return "br" if (store.locale === "br") return "br"
if (store.locale === "th") return "th"
return "en" return "en"
}) })
@@ -118,6 +153,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
if (locale() === "ar") return { ...base, ...i18n.flatten({ ...ar, ...uiAr }) } if (locale() === "ar") return { ...base, ...i18n.flatten({ ...ar, ...uiAr }) }
if (locale() === "no") return { ...base, ...i18n.flatten({ ...no, ...uiNo }) } if (locale() === "no") return { ...base, ...i18n.flatten({ ...no, ...uiNo }) }
if (locale() === "br") return { ...base, ...i18n.flatten({ ...br, ...uiBr }) } if (locale() === "br") return { ...base, ...i18n.flatten({ ...br, ...uiBr }) }
if (locale() === "th") return { ...base, ...i18n.flatten({ ...th, ...uiTh }) }
return { ...base, ...i18n.flatten({ ...ko, ...uiKo }) } return { ...base, ...i18n.flatten({ ...ko, ...uiKo }) }
}) })
@@ -138,6 +174,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
ar: "language.ar", ar: "language.ar",
no: "language.no", no: "language.no",
br: "language.br", br: "language.br",
th: "language.th",
} }
const label = (value: Locale) => t(labelKey[value]) const label = (value: Locale) => t(labelKey[value])
+3 -10
View File
@@ -624,10 +624,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] }) const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] })
return { return {
tabs, tabs,
active: createMemo(() => tabs().active), active: createMemo(() => (tabs().active === "review" ? undefined : tabs().active)),
all: createMemo(() => tabs().all.filter((tab) => tab !== "review")), all: createMemo(() => tabs().all.filter((tab) => tab !== "review")),
setActive(tab: string | undefined) { setActive(tab: string | undefined) {
const session = key() const session = key()
if (tab === "review") return
if (!store.sessionTabs[session]) { if (!store.sessionTabs[session]) {
setStore("sessionTabs", session, { all: [], active: tab }) setStore("sessionTabs", session, { all: [], active: tab })
} else { } else {
@@ -644,18 +645,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
} }
}, },
async open(tab: string) { async open(tab: string) {
if (tab === "review") return
const session = key() const session = key()
const current = store.sessionTabs[session] ?? { all: [] } 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") { if (tab === "context") {
const all = [tab, ...current.all.filter((x) => x !== tab)] const all = [tab, ...current.all.filter((x) => x !== tab)]
if (!store.sessionTabs[session]) { if (!store.sessionTabs[session]) {
+1
View File
@@ -331,6 +331,7 @@ export const dict = {
"language.ar": "العربية", "language.ar": "العربية",
"language.no": "Norsk", "language.no": "Norsk",
"language.br": "Português (Brasil)", "language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "لغة", "toast.language.title": "لغة",
"toast.language.description": "تم التبديل إلى {{language}}", "toast.language.description": "تم التبديل إلى {{language}}",
+1
View File
@@ -330,6 +330,7 @@ export const dict = {
"language.ar": "العربية", "language.ar": "العربية",
"language.no": "Norsk", "language.no": "Norsk",
"language.br": "Português (Brasil)", "language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Idioma", "toast.language.title": "Idioma",
"toast.language.description": "Alterado para {{language}}", "toast.language.description": "Alterado para {{language}}",
+1
View File
@@ -332,6 +332,7 @@ export const dict = {
"language.ar": "العربية", "language.ar": "العربية",
"language.no": "Norsk", "language.no": "Norsk",
"language.br": "Português (Brasil)", "language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Sprog", "toast.language.title": "Sprog",
"toast.language.description": "Skiftede til {{language}}", "toast.language.description": "Skiftede til {{language}}",
+1
View File
@@ -338,6 +338,7 @@ export const dict = {
"language.ar": "العربية", "language.ar": "العربية",
"language.no": "Norsk", "language.no": "Norsk",
"language.br": "Português (Brasil)", "language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Sprache", "toast.language.title": "Sprache",
"toast.language.description": "Zu {{language}} gewechselt", "toast.language.description": "Zu {{language}} gewechselt",
+1
View File
@@ -337,6 +337,7 @@ export const dict = {
"language.ar": "العربية", "language.ar": "العربية",
"language.no": "Norsk", "language.no": "Norsk",
"language.br": "Português (Brasil)", "language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Language", "toast.language.title": "Language",
"toast.language.description": "Switched to {{language}}", "toast.language.description": "Switched to {{language}}",
+1
View File
@@ -333,6 +333,7 @@ export const dict = {
"language.ar": "العربية", "language.ar": "العربية",
"language.no": "Norsk", "language.no": "Norsk",
"language.br": "Português (Brasil)", "language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Idioma", "toast.language.title": "Idioma",
"toast.language.description": "Cambiado a {{language}}", "toast.language.description": "Cambiado a {{language}}",
+1
View File
@@ -333,6 +333,7 @@ export const dict = {
"language.ar": "العربية", "language.ar": "العربية",
"language.no": "Norsk", "language.no": "Norsk",
"language.br": "Português (Brasil)", "language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Langue", "toast.language.title": "Langue",
"toast.language.description": "Passé à {{language}}", "toast.language.description": "Passé à {{language}}",
+1
View File
@@ -331,6 +331,7 @@ export const dict = {
"language.ar": "العربية", "language.ar": "العربية",
"language.no": "Norsk", "language.no": "Norsk",
"language.br": "Português (Brasil)", "language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "言語", "toast.language.title": "言語",
"toast.language.description": "{{language}}に切り替えました", "toast.language.description": "{{language}}に切り替えました",
+1
View File
@@ -334,6 +334,7 @@ export const dict = {
"language.ar": "العربية", "language.ar": "العربية",
"language.no": "Norsk", "language.no": "Norsk",
"language.br": "Português (Brasil)", "language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "언어", "toast.language.title": "언어",
"toast.language.description": "{{language}}(으)로 전환됨", "toast.language.description": "{{language}}(으)로 전환됨",
+1
View File
@@ -334,6 +334,7 @@ export const dict = {
"language.ar": "العربية", "language.ar": "العربية",
"language.no": "Norsk", "language.no": "Norsk",
"language.br": "Português (Brasil)", "language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Språk", "toast.language.title": "Språk",
"toast.language.description": "Byttet til {{language}}", "toast.language.description": "Byttet til {{language}}",
+1
View File
@@ -332,6 +332,7 @@ export const dict = {
"language.ar": "العربية", "language.ar": "العربية",
"language.no": "Norsk", "language.no": "Norsk",
"language.br": "Português (Brasil)", "language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Język", "toast.language.title": "Język",
"toast.language.description": "Przełączono na {{language}}", "toast.language.description": "Przełączono na {{language}}",
+1
View File
@@ -333,6 +333,7 @@ export const dict = {
"language.ar": "العربية", "language.ar": "العربية",
"language.no": "Norsk", "language.no": "Norsk",
"language.br": "Português (Brasil)", "language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Язык", "toast.language.title": "Язык",
"toast.language.description": "Переключено на {{language}}", "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.palette": "命令面板",
"command.theme.cycle": "切换主题", "command.theme.cycle": "切换主题",
"command.theme.set": "使用主题: {{theme}}", "command.theme.set": "使用主题{{theme}}",
"command.theme.scheme.cycle": "切换配色方案", "command.theme.scheme.cycle": "切换配色方案",
"command.theme.scheme.set": "使用配色方案: {{scheme}}", "command.theme.scheme.set": "使用配色方案{{scheme}}",
"command.language.cycle": "切换语言", "command.language.cycle": "切换语言",
"command.language.set": "使用语言: {{language}}", "command.language.set": "使用语言{{language}}",
"command.session.new": "新建会话", "command.session.new": "新建会话",
"command.file.open": "打开文件", "command.file.open": "打开文件",
@@ -98,6 +98,10 @@ export const dict = {
"dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 密钥连接", "dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 密钥连接",
"dialog.provider.openai.note": "使用 ChatGPT Pro/Plus 或 API 密钥连接", "dialog.provider.openai.note": "使用 ChatGPT Pro/Plus 或 API 密钥连接",
"dialog.provider.copilot.note": "使用 Copilot 或 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.select.title": "选择模型",
"dialog.model.search.placeholder": "搜索模型", "dialog.model.search.placeholder": "搜索模型",
@@ -116,7 +120,7 @@ export const dict = {
"provider.connect.method.apiKey": "API 密钥", "provider.connect.method.apiKey": "API 密钥",
"provider.connect.status.inProgress": "正在授权...", "provider.connect.status.inProgress": "正在授权...",
"provider.connect.status.waiting": "等待授权...", "provider.connect.status.waiting": "等待授权...",
"provider.connect.status.failed": "授权失败: {{error}}", "provider.connect.status.failed": "授权失败{{error}}",
"provider.connect.apiKey.description": "provider.connect.apiKey.description":
"输入你的 {{provider}} API 密钥以连接帐户,并在 OpenCode 中使用 {{provider}} 模型。", "输入你的 {{provider}} API 密钥以连接帐户,并在 OpenCode 中使用 {{provider}} 模型。",
"provider.connect.apiKey.label": "{{provider}} API 密钥", "provider.connect.apiKey.label": "{{provider}} API 密钥",
@@ -156,7 +160,7 @@ export const dict = {
"model.input.audio": "音频", "model.input.audio": "音频",
"model.input.video": "视频", "model.input.video": "视频",
"model.input.pdf": "pdf", "model.input.pdf": "pdf",
"model.tooltip.allows": "支持: {{inputs}}", "model.tooltip.allows": "支持{{inputs}}",
"model.tooltip.reasoning.allowed": "支持推理", "model.tooltip.reasoning.allowed": "支持推理",
"model.tooltip.reasoning.none": "不支持推理", "model.tooltip.reasoning.none": "不支持推理",
"model.tooltip.context": "上下文上限 {{limit}}", "model.tooltip.context": "上下文上限 {{limit}}",
@@ -181,30 +185,30 @@ export const dict = {
"prompt.mode.shell.exit": "按 esc 退出", "prompt.mode.shell.exit": "按 esc 退出",
"prompt.example.1": "修复代码库中的一个 TODO", "prompt.example.1": "修复代码库中的一个 TODO",
"prompt.example.2": "这个项目的技术栈是什么?", "prompt.example.2": "这个项目的技术栈是什么",
"prompt.example.3": "修复失败的测试", "prompt.example.3": "修复失败的测试",
"prompt.example.4": "解释认证是如何工作的", "prompt.example.4": "解释认证是如何工作的",
"prompt.example.5": "查找并修复安全漏洞", "prompt.example.5": "查找并修复安全漏洞",
"prompt.example.6": "为用户服务添加单元测试", "prompt.example.6": "为用户服务添加单元测试",
"prompt.example.7": "重构这个函数,让它更易读", "prompt.example.7": "重构这个函数,让它更易读",
"prompt.example.8": "这个错误是什么意思?", "prompt.example.8": "这个错误是什么意思",
"prompt.example.9": "帮我调试这个问题", "prompt.example.9": "帮我调试这个问题",
"prompt.example.10": "生成 API 文档", "prompt.example.10": "生成 API 文档",
"prompt.example.11": "优化数据库查询", "prompt.example.11": "优化数据库查询",
"prompt.example.12": "添加输入校验", "prompt.example.12": "添加输入校验",
"prompt.example.13": "创建一个新的组件用于...", "prompt.example.13": "创建一个新的组件用于...",
"prompt.example.14": "我该如何部署这个项目?", "prompt.example.14": "我该如何部署这个项目",
"prompt.example.15": "审查我的代码并给出最佳实践建议", "prompt.example.15": "审查我的代码并给出最佳实践建议",
"prompt.example.16": "为这个函数添加错误处理", "prompt.example.16": "为这个函数添加错误处理",
"prompt.example.17": "解释这个正则表达式", "prompt.example.17": "解释这个正则表达式",
"prompt.example.18": "把它转换成 TypeScript", "prompt.example.18": "把它转换成 TypeScript",
"prompt.example.19": "在整个代码库中添加日志", "prompt.example.19": "在整个代码库中添加日志",
"prompt.example.20": "哪些依赖已经过期?", "prompt.example.20": "哪些依赖已经过期",
"prompt.example.21": "帮我写一个迁移脚本", "prompt.example.21": "帮我写一个迁移脚本",
"prompt.example.22": "为这个接口实现缓存", "prompt.example.22": "为这个接口实现缓存",
"prompt.example.23": "给这个列表添加分页", "prompt.example.23": "给这个列表添加分页",
"prompt.example.24": "创建一个 CLI 命令用于...", "prompt.example.24": "创建一个 CLI 命令用于...",
"prompt.example.25": "这里的环境变量是怎么工作的?", "prompt.example.25": "这里的环境变量是怎么工作的",
"prompt.popover.emptyResults": "没有匹配的结果", "prompt.popover.emptyResults": "没有匹配的结果",
"prompt.popover.emptyCommands": "没有匹配的命令", "prompt.popover.emptyCommands": "没有匹配的命令",
@@ -330,6 +334,7 @@ export const dict = {
"language.ar": "العربية", "language.ar": "العربية",
"language.no": "Norsk", "language.no": "Norsk",
"language.br": "Português (Brasil)", "language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "语言", "toast.language.title": "语言",
"toast.language.description": "已切换到{{language}}", "toast.language.description": "已切换到{{language}}",
@@ -377,31 +382,31 @@ export const dict = {
"error.page.action.updateTo": "更新到 {{version}}", "error.page.action.updateTo": "更新到 {{version}}",
"error.page.report.prefix": "请将此错误报告给 OpenCode 团队", "error.page.report.prefix": "请将此错误报告给 OpenCode 团队",
"error.page.report.discord": "在 Discord 上", "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.unknown": "未知错误",
"error.chain.causedBy": "原因:", "error.chain.causedBy": "原因",
"error.chain.apiError": "API 错误", "error.chain.apiError": "API 错误",
"error.chain.status": "状态: {{status}}", "error.chain.status": "状态{{status}}",
"error.chain.retryable": "可重试: {{retryable}}", "error.chain.retryable": "可重试{{retryable}}",
"error.chain.responseBody": "响应内容:\n{{body}}", "error.chain.responseBody": "响应内容\n{{body}}",
"error.chain.didYouMean": "你是不是想输入: {{suggestions}}", "error.chain.didYouMean": "你是不是想输入{{suggestions}}",
"error.chain.modelNotFound": "未找到模型: {{provider}}/{{model}}", "error.chain.modelNotFound": "未找到模型{{provider}}/{{model}}",
"error.chain.checkConfig": "请检查你的配置 (opencode.json) 中的 provider/model 名称", "error.chain.checkConfig": "请检查你的配置 (opencode.json) 中的 provider/model 名称",
"error.chain.mcpFailed": 'MCP 服务器 "{{name}}" 启动失败。注意: OpenCode 暂不支持 MCP 认证。', "error.chain.mcpFailed": 'MCP 服务器 "{{name}}" 启动失败。注意: OpenCode 暂不支持 MCP 认证。',
"error.chain.providerAuthFailed": "提供商认证失败 ({{provider}}): {{message}}", "error.chain.providerAuthFailed": "提供商认证失败{{provider}}):{{message}}",
"error.chain.providerInitFailed": '无法初始化提供商 "{{provider}}"。请检查凭据和配置。', "error.chain.providerInitFailed": '无法初始化提供商 "{{provider}}"。请检查凭据和配置。',
"error.chain.configJsonInvalid": "配置文件 {{path}} 不是有效的 JSON(C)", "error.chain.configJsonInvalid": "配置文件 {{path}} 不是有效的 JSON(C)",
"error.chain.configJsonInvalidWithMessage": "配置文件 {{path}} 不是有效的 JSON(C): {{message}}", "error.chain.configJsonInvalidWithMessage": "配置文件 {{path}} 不是有效的 JSON(C){{message}}",
"error.chain.configDirectoryTypo": "error.chain.configDirectoryTypo":
'{{path}} 中的目录 "{{dir}}" 无效。请将目录重命名为 "{{suggestion}}" 或移除它。这是一个常见拼写错误。', '{{path}} 中的目录 "{{dir}}" 无效。请将目录重命名为 "{{suggestion}}" 或移除它。这是一个常见拼写错误。',
"error.chain.configFrontmatterError": "无法解析 {{path}} 中的 frontmatter:\n{{message}}", "error.chain.configFrontmatterError": "无法解析 {{path}} 中的 frontmatter\n{{message}}",
"error.chain.configInvalid": "配置文件 {{path}} 无效", "error.chain.configInvalid": "配置文件 {{path}} 无效",
"error.chain.configInvalidWithMessage": "配置文件 {{path}} 无效: {{message}}", "error.chain.configInvalidWithMessage": "配置文件 {{path}} 无效{{message}}",
"notification.permission.title": "需要权限", "notification.permission.title": "需要权限",
"notification.permission.description": "{{sessionTitle}}{{projectName}})需要权限", "notification.permission.description": "{{sessionTitle}}{{projectName}})需要权限",
@@ -438,7 +443,7 @@ export const dict = {
"session.context.addToContext": "将 {{selection}} 添加到上下文", "session.context.addToContext": "将 {{selection}} 添加到上下文",
"session.new.worktree.main": "主分支", "session.new.worktree.main": "主分支",
"session.new.worktree.mainWithBranch": "主分支 ({{branch}})", "session.new.worktree.mainWithBranch": "主分支{{branch}}",
"session.new.worktree.create": "创建新的 worktree", "session.new.worktree.create": "创建新的 worktree",
"session.new.lastModified": "最后修改", "session.new.lastModified": "最后修改",
@@ -521,7 +526,6 @@ export const dict = {
"settings.general.row.theme.description": "自定义 OpenCode 的主题。", "settings.general.row.theme.description": "自定义 OpenCode 的主题。",
"settings.general.row.font.title": "字体", "settings.general.row.font.title": "字体",
"settings.general.row.font.description": "自定义代码块使用的等宽字体", "settings.general.row.font.description": "自定义代码块使用的等宽字体",
"settings.general.row.releaseNotes.title": "发行说明", "settings.general.row.releaseNotes.title": "发行说明",
"settings.general.row.releaseNotes.description": "更新后显示“新功能”弹窗", "settings.general.row.releaseNotes.description": "更新后显示“新功能”弹窗",
@@ -685,7 +689,7 @@ export const dict = {
"session.delete.failed.title": "删除会话失败", "session.delete.failed.title": "删除会话失败",
"session.delete.title": "删除会话", "session.delete.title": "删除会话",
"session.delete.confirm": '删除会话 "{{name}}"?', "session.delete.confirm": '删除会话 "{{name}}"',
"session.delete.button": "删除会话", "session.delete.button": "删除会话",
"workspace.new": "新建工作区", "workspace.new": "新建工作区",
@@ -704,10 +708,10 @@ export const dict = {
"workspace.status.clean": "未检测到未合并的更改。", "workspace.status.clean": "未检测到未合并的更改。",
"workspace.status.dirty": "检测到未合并的更改。", "workspace.status.dirty": "检测到未合并的更改。",
"workspace.delete.title": "删除工作区", "workspace.delete.title": "删除工作区",
"workspace.delete.confirm": '删除工作区 "{{name}}"?', "workspace.delete.confirm": '删除工作区 "{{name}}"',
"workspace.delete.button": "删除工作区", "workspace.delete.button": "删除工作区",
"workspace.reset.title": "重置工作区", "workspace.reset.title": "重置工作区",
"workspace.reset.confirm": '重置工作区 "{{name}}"?', "workspace.reset.confirm": '重置工作区 "{{name}}"',
"workspace.reset.button": "重置工作区", "workspace.reset.button": "重置工作区",
"workspace.reset.archived.none": "不会归档任何活跃会话。", "workspace.reset.archived.none": "不会归档任何活跃会话。",
"workspace.reset.archived.one": "将归档 1 个会话。", "workspace.reset.archived.one": "将归档 1 个会话。",
+1
View File
@@ -331,6 +331,7 @@ export const dict = {
"language.ar": "العربية", "language.ar": "العربية",
"language.no": "Norsk", "language.no": "Norsk",
"language.br": "Português (Brasil)", "language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "語言", "toast.language.title": "語言",
"toast.language.description": "已切換到 {{language}}", "toast.language.description": "已切換到 {{language}}",
+86 -66
View File
@@ -23,7 +23,6 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" 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 { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Tabs } from "@opencode-ai/ui/tabs" import { Tabs } from "@opencode-ai/ui/tabs"
import { useCodeComponent } from "@opencode-ai/ui/context/code" import { useCodeComponent } from "@opencode-ai/ui/context/code"
@@ -325,6 +324,7 @@ export default function Page() {
} }
const isDesktop = createMediaQuery("(min-width: 768px)") const isDesktop = createMediaQuery("(min-width: 768px)")
const centered = createMemo(() => isDesktop() && !layout.fileTree.opened())
function normalizeTab(tab: string) { function normalizeTab(tab: string) {
if (!tab.startsWith("file://")) return tab if (!tab.startsWith("file://")) return tab
@@ -730,8 +730,8 @@ export default function Page() {
onSelect: () => view().terminal.toggle(), onSelect: () => view().terminal.toggle(),
}, },
{ {
id: "fileTree.toggle", id: "review.toggle",
title: language.t("command.fileTree.toggle"), title: language.t("command.review.toggle"),
description: "", description: "",
category: language.t("command.category.view"), category: language.t("command.category.view"),
keybind: "mod+shift+r", keybind: "mod+shift+r",
@@ -1097,12 +1097,11 @@ export default function Page() {
}, 0) }, 0)
} }
const reviewTab = createMemo(() => isDesktop() && !layout.fileTree.opened())
const contextOpen = createMemo(() => tabs().active() === "context" || tabs().all().includes("context")) const contextOpen = createMemo(() => tabs().active() === "context" || tabs().all().includes("context"))
const openedTabs = createMemo(() => const openedTabs = createMemo(() =>
tabs() tabs()
.all() .all()
.filter((tab) => tab !== "context" && tab !== "review"), .filter((tab) => tab !== "context"),
) )
const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes") const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
@@ -1271,56 +1270,29 @@ export default function Page() {
const activeTab = createMemo(() => { const activeTab = createMemo(() => {
const active = tabs().active() const active = tabs().active()
if (active === "context") return "context" if (active === "context") return "context"
if (active === "review" && reviewTab()) return "review"
if (active && file.pathFromTab(active)) return normalizeTab(active) if (active && file.pathFromTab(active)) return normalizeTab(active)
const first = openedTabs()[0] const first = openedTabs()[0]
if (first) return first if (first) return first
if (contextOpen()) return "context" if (contextOpen()) return "context"
if (reviewTab() && hasReview()) return "review"
return "empty" return "empty"
}) })
createEffect(() => { createEffect(() => {
if (!layout.ready()) return if (!layout.ready()) return
if (tabs().active()) return if (tabs().active()) return
if (openedTabs().length === 0 && !contextOpen() && !(reviewTab() && hasReview())) return if (openedTabs().length === 0 && !contextOpen()) return
const next = activeTab() const next = activeTab()
if (next === "empty") return if (next === "empty") return
tabs().setActive(next) 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(() => { createEffect(() => {
const id = params.id const id = params.id
if (!id) return if (!id) return
const wants = isDesktop() const wants = isDesktop() ? layout.fileTree.opened() && fileTreeTab() === "changes" : store.mobileTab === "changes"
? layout.fileTree.opened()
? fileTreeTab() === "changes"
: activeTab() === "review"
: store.mobileTab === "changes"
if (!wants) return if (!wants) return
if (sync.data.session_diff[id] !== undefined) return if (sync.data.session_diff[id] !== undefined) return
if (sync.status === "loading") return if (sync.status === "loading") return
@@ -1789,10 +1761,11 @@ export default function Page() {
<div <div
classList={{ classList={{
"@container relative shrink-0 flex flex-col min-h-0 h-full bg-background-stronger": true, "@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={{ 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, "--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, "sticky top-0 z-30 bg-background-stronger": true,
"w-full": true, "w-full": true,
"px-4 md:px-6": true, "px-4 md:px-6": true,
"md:max-w-200 md:mx-auto": centered(),
}} }}
> >
<div class="h-10 flex items-center gap-1"> <div class="h-10 flex items-center gap-1">
@@ -1991,7 +1965,13 @@ export default function Page() {
<div <div
ref={autoScroll.contentRef} ref={autoScroll.contentRef}
role="log" 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}> <Show when={store.turnStart > 0}>
<div class="w-full flex justify-center"> <div class="w-full flex justify-center">
@@ -2039,7 +2019,10 @@ export default function Page() {
<div <div
id={anchor(message.id)} id={anchor(message.id)}
data-message-id={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 <SessionTurn
sessionID={params.id!} sessionID={params.id!}
@@ -2092,7 +2075,12 @@ export default function Page() {
ref={(el) => (promptDock = el)} 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" 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> <Show when={request()} keyed>
{(perm) => ( {(perm) => (
<div data-component="tool-part-wrapper" data-permission="true" class="mb-3"> <div data-component="tool-part-wrapper" data-permission="true" class="mb-3">
@@ -2163,7 +2151,7 @@ export default function Page() {
</div> </div>
</div> </div>
<Show when={isDesktop()}> <Show when={isDesktop() && layout.fileTree.opened()}>
<ResizeHandle <ResizeHandle
direction="horizontal" direction="horizontal"
size={layout.session.width()} size={layout.session.width()}
@@ -2175,7 +2163,7 @@ export default function Page() {
</div> </div>
{/* Desktop side panel - hidden on mobile */} {/* Desktop side panel - hidden on mobile */}
<Show when={isDesktop()}> <Show when={isDesktop() && layout.fileTree.opened()}>
<aside <aside
id="review-panel" id="review-panel"
aria-label={language.t("session.panel.reviewAndFiles")} aria-label={language.t("session.panel.reviewAndFiles")}
@@ -2183,7 +2171,7 @@ export default function Page() {
> >
<div class="flex-1 min-w-0 h-full"> <div class="flex-1 min-w-0 h-full">
<Show <Show
when={layout.fileTree.opened() && fileTreeTab() === "changes"} when={fileTreeTab() === "changes"}
fallback={ fallback={
<DragDropProvider <DragDropProvider
onDragStart={handleDragStart} onDragStart={handleDragStart}
@@ -2195,24 +2183,62 @@ export default function Page() {
<ConstrainDragYAxis /> <ConstrainDragYAxis />
<Tabs value={activeTab()} onChange={openTab}> <Tabs value={activeTab()} onChange={openTab}>
<div class="sticky top-0 shrink-0 flex"> <div class="sticky top-0 shrink-0 flex">
<Tabs.List> <Tabs.List
<Show when={reviewTab()}> ref={(el: HTMLDivElement) => {
<Tabs.Trigger value="review" classes={{ button: "!pl-6" }}> let scrollTimeout: number | undefined
<div class="flex items-center gap-3"> let prevScrollWidth = el.scrollWidth
<Show when={hasReview() && diffsReady()}> let prevContextOpen = contextOpen()
<DiffChanges changes={diffs()} variant="bars" />
</Show> const handler = () => {
<div class="flex items-center gap-1.5"> if (scrollTimeout !== undefined) clearTimeout(scrollTimeout)
<div>{language.t("session.tab.review")}</div> scrollTimeout = window.setTimeout(() => {
<Show when={hasReview()}> const scrollWidth = el.scrollWidth
<div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base"> const clientWidth = el.clientWidth
{reviewCount()} const currentContextOpen = contextOpen()
</div>
</Show> // Only scroll when a tab is added (width increased), not on removal
</div> if (scrollWidth > prevScrollWidth) {
</div> if (!prevContextOpen && currentContextOpen) {
</Tabs.Trigger> // Context tab was opened, scroll to first
</Show> 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()}> <Show when={contextOpen()}>
<Tabs.Trigger <Tabs.Trigger
value="context" value="context"
@@ -2261,12 +2287,6 @@ export default function Page() {
</Tabs.List> </Tabs.List>
</div> </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"> <Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={activeTab() === "empty"}> <Show when={activeTab() === "empty"}>
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden"> <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", "name": "@opencode-ai/console-app",
"version": "1.1.38", "version": "1.1.42",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.1.38", "version": "1.1.42",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.1.38", "version": "1.1.42",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.1.38", "version": "1.1.42",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"private": true, "private": true,
"version": "1.1.38", "version": "1.1.42",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.1.38", "version": "1.1.42",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode" id = "opencode"
name = "OpenCode" name = "OpenCode"
description = "The open source coding agent." description = "The open source coding agent."
version = "1.1.38" version = "1.1.42"
schema_version = 1 schema_version = 1
authors = ["Anomaly"] authors = ["Anomaly"]
repository = "https://github.com/anomalyco/opencode" repository = "https://github.com/anomalyco/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg" icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64] [agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.38/opencode-darwin-arm64.zip" archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.42/opencode-darwin-arm64.zip"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64] [agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.38/opencode-darwin-x64.zip" archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.42/opencode-darwin-x64.zip"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64] [agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.38/opencode-linux-arm64.tar.gz" archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.42/opencode-linux-arm64.tar.gz"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64] [agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.38/opencode-linux-x64.tar.gz" archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.42/opencode-linux-x64.tar.gz"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64] [agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.38/opencode-windows-x64.zip" archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.42/opencode-windows-x64.zip"
cmd = "./opencode.exe" cmd = "./opencode.exe"
args = ["acp"] args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.1.38", "version": "1.1.42",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.1.38", "version": "1.1.42",
"name": "opencode", "name": "opencode",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+2 -5
View File
@@ -1,6 +1,5 @@
import path from "path" import path from "path"
import { Global } from "../global" import { Global } from "../global"
import fs from "fs/promises"
import z from "zod" import z from "zod"
export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key" export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key"
@@ -59,15 +58,13 @@ export namespace Auth {
export async function set(key: string, info: Info) { export async function set(key: string, info: Info) {
const file = Bun.file(filepath) const file = Bun.file(filepath)
const data = await all() const data = await all()
await Bun.write(file, JSON.stringify({ ...data, [key]: info }, null, 2)) await Bun.write(file, JSON.stringify({ ...data, [key]: info }, null, 2), { mode: 0o600 })
await fs.chmod(file.name!, 0o600)
} }
export async function remove(key: string) { export async function remove(key: string) {
const file = Bun.file(filepath) const file = Bun.file(filepath)
const data = await all() const data = await all()
delete data[key] delete data[key]
await Bun.write(file, JSON.stringify(data, null, 2)) await Bun.write(file, JSON.stringify(data, null, 2), { mode: 0o600 })
await fs.chmod(file.name!, 0o600)
} }
} }
+28 -4
View File
@@ -27,6 +27,10 @@ interface SessionStats {
tokens: { tokens: {
input: number input: number
output: number output: number
cache: {
read: number
write: number
}
} }
cost: number cost: number
} }
@@ -191,6 +195,10 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
tokens: { tokens: {
input: number input: number
output: number output: number
cache: {
read: number
write: number
}
} }
cost: number cost: number
} }
@@ -204,7 +212,7 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
if (!sessionModelUsage[modelKey]) { if (!sessionModelUsage[modelKey]) {
sessionModelUsage[modelKey] = { sessionModelUsage[modelKey] = {
messages: 0, messages: 0,
tokens: { input: 0, output: 0 }, tokens: { input: 0, output: 0, cache: { read: 0, write: 0 } },
cost: 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.input += message.info.tokens.input || 0
sessionModelUsage[modelKey].tokens.output += sessionModelUsage[modelKey].tokens.output +=
(message.info.tokens.output || 0) + (message.info.tokens.reasoning || 0) (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, messageCount: messages.length,
sessionCost, sessionCost,
sessionTokens, sessionTokens,
sessionTotalTokens: sessionTokens.input + sessionTokens.output + sessionTokens.reasoning, sessionTotalTokens:
sessionTokens.input +
sessionTokens.output +
sessionTokens.reasoning +
sessionTokens.cache.read +
sessionTokens.cache.write,
sessionToolUsage, sessionToolUsage,
sessionModelUsage, sessionModelUsage,
earliestTime: cutoffTime > 0 ? session.time.updated : session.time.created, 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]) { if (!stats.modelUsage[model]) {
stats.modelUsage[model] = { stats.modelUsage[model] = {
messages: 0, messages: 0,
tokens: { input: 0, output: 0 }, tokens: { input: 0, output: 0, cache: { read: 0, write: 0 } },
cost: 0, cost: 0,
} }
} }
stats.modelUsage[model].messages += usage.messages stats.modelUsage[model].messages += usage.messages
stats.modelUsage[model].tokens.input += usage.tokens.input stats.modelUsage[model].tokens.input += usage.tokens.input
stats.modelUsage[model].tokens.output += usage.tokens.output 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 stats.modelUsage[model].cost += usage.cost
} }
} }
@@ -286,7 +303,12 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
} }
stats.days = effectiveDays stats.days = effectiveDays
stats.costPerDay = stats.totalCost / 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 stats.tokensPerSession = filteredSessions.length > 0 ? totalTokens / filteredSessions.length : 0
sessionTotalTokens.sort((a, b) => a - b) sessionTotalTokens.sort((a, b) => a - b)
const mid = Math.floor(sessionTotalTokens.length / 2) 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(" Messages", usage.messages.toLocaleString()))
console.log(renderRow(" Input Tokens", formatNumber(usage.tokens.input))) console.log(renderRow(" Input Tokens", formatNumber(usage.tokens.input)))
console.log(renderRow(" Output Tokens", formatNumber(usage.tokens.output))) 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(renderRow(" Cost", `$${usage.cost.toFixed(4)}`))
console.log("├────────────────────────────────────────────────────────┤") console.log("├────────────────────────────────────────────────────────┤")
} }
@@ -93,8 +93,11 @@ export function Prompt(props: PromptProps) {
let promptPartTypeId = 0 let promptPartTypeId = 0
sdk.event.on(TuiEvent.PromptAppend.type, (evt) => { sdk.event.on(TuiEvent.PromptAppend.type, (evt) => {
if (!input || input.isDestroyed) return
input.insertText(evt.properties.text) input.insertText(evt.properties.text)
setTimeout(() => { setTimeout(() => {
// setTimeout is a workaround and needs to be addressed properly
if (!input || input.isDestroyed) return
input.getLayoutNode().markDirty() input.getLayoutNode().markDirty()
input.gotoBufferEnd() input.gotoBufferEnd()
renderer.requestRender() renderer.requestRender()
@@ -924,6 +927,8 @@ export function Prompt(props: PromptProps) {
// Force layout update and render for the pasted content // Force layout update and render for the pasted content
setTimeout(() => { setTimeout(() => {
// setTimeout is a workaround and needs to be addressed properly
if (!input || input.isDestroyed) return
input.getLayoutNode().markDirty() input.getLayoutNode().markDirty()
renderer.requestRender() renderer.requestRender()
}, 0) }, 0)
@@ -935,6 +940,8 @@ export function Prompt(props: PromptProps) {
} }
props.ref?.(ref) props.ref?.(ref)
setTimeout(() => { setTimeout(() => {
// setTimeout is a workaround and needs to be addressed properly
if (!input || input.isDestroyed) return
input.cursorColor = theme.text input.cursorColor = theme.text
}, 0) }, 0)
}} }}
@@ -34,9 +34,8 @@ export const { use: useKeybind, provider: KeybindProvider } = createSimpleContex
timeout = setTimeout(() => { timeout = setTimeout(() => {
if (!store.leader) return if (!store.leader) return
leader(false) leader(false)
if (focus) { if (!focus || focus.isDestroyed) return
focus.focus() focus.focus()
}
}, 2000) }, 2000)
return return
} }
@@ -275,7 +275,8 @@ export function Session() {
function toBottom() { function toBottom() {
setTimeout(() => { setTimeout(() => {
if (scroll) scroll.scrollTo(scroll.scrollHeight) if (!scroll || scroll.isDestroyed) return
scroll.scrollTo(scroll.scrollHeight)
}, 50) }, 50)
} }
@@ -1341,7 +1342,12 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0}> <box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0}>
<Switch> <Switch>
<Match when={Flag.OPENCODE_EXPERIMENTAL_MARKDOWN}> <Match when={Flag.OPENCODE_EXPERIMENTAL_MARKDOWN}>
<markdown syntaxStyle={syntax()} content={props.part.text.trim()} conceal={ctx.conceal()} /> <markdown
syntaxStyle={syntax()}
streaming={true}
content={props.part.text.trim()}
conceal={ctx.conceal()}
/>
</Match> </Match>
<Match when={!Flag.OPENCODE_EXPERIMENTAL_MARKDOWN}> <Match when={!Flag.OPENCODE_EXPERIMENTAL_MARKDOWN}>
<code <code
@@ -68,6 +68,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
onMount(() => { onMount(() => {
dialog.setSize("medium") dialog.setSize("medium")
setTimeout(() => { setTimeout(() => {
if (!textarea || textarea.isDestroyed) return
textarea.focus() textarea.focus()
}, 1) }, 1)
textarea.gotoLineEnd() textarea.gotoLineEnd()
@@ -27,6 +27,7 @@ export function DialogPrompt(props: DialogPromptProps) {
onMount(() => { onMount(() => {
dialog.setSize("medium") dialog.setSize("medium")
setTimeout(() => { setTimeout(() => {
if (!textarea || textarea.isDestroyed) return
textarea.focus() textarea.focus()
}, 1) }, 1)
textarea.gotoLineEnd() textarea.gotoLineEnd()
@@ -241,7 +241,11 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
focusedTextColor={theme.textMuted} focusedTextColor={theme.textMuted}
ref={(r) => { ref={(r) => {
input = r input = r
setTimeout(() => input.focus(), 1) setTimeout(() => {
if (!input) return
if (input.isDestroyed) return
input.focus()
}, 1)
}} }}
placeholder={props.placeholder ?? "Search"} placeholder={props.placeholder ?? "Search"}
/> />
+43 -29
View File
@@ -1104,20 +1104,23 @@ export namespace Config {
mergeDeep(await loadFile(path.join(Global.Path.config, "opencode.jsonc"))), mergeDeep(await loadFile(path.join(Global.Path.config, "opencode.jsonc"))),
) )
await import(path.join(Global.Path.config, "config"), { const legacy = path.join(Global.Path.config, "config")
with: { if (existsSync(legacy)) {
type: "toml", await import(pathToFileURL(legacy).href, {
}, 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"))
}) })
.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 return result
}) })
@@ -1341,24 +1344,35 @@ export namespace Config {
throw new JsonError({ path: filepath }, { cause: err }) throw new JsonError({ path: filepath }, { cause: err })
}) })
if (!filepath.endsWith(".jsonc")) { const next = await (async () => {
const existing = parseConfig(before, filepath) if (!filepath.endsWith(".jsonc")) {
await Bun.write(filepath, JSON.stringify(mergeDeep(existing, config), null, 2)) const existing = parseConfig(before, filepath)
} else { const merged = mergeDeep(existing, config)
const next = patchJsonc(before, config) await Bun.write(filepath, JSON.stringify(merged, null, 2))
parseConfig(next, filepath) return merged
await Bun.write(filepath, next) }
}
const updated = patchJsonc(before, config)
const merged = parseConfig(updated, filepath)
await Bun.write(filepath, updated)
return merged
})()
global.reset() global.reset()
await Instance.disposeAll()
GlobalBus.emit("event", { void Instance.disposeAll()
directory: "global", .catch(() => undefined)
payload: { .finally(() => {
type: Event.Disposed.type, GlobalBus.emit("event", {
properties: {}, directory: "global",
}, payload: {
}) type: Event.Disposed.type,
properties: {},
},
})
})
return next
} }
export async function directories() { export async function directories() {
+17 -12
View File
@@ -14,7 +14,9 @@ export namespace ConfigMarkdown {
return Array.from(template.matchAll(SHELL_REGEX)) 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---/) const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
if (!match) return content if (!match) return content
@@ -53,7 +55,7 @@ export namespace ConfigMarkdown {
// if value contains a colon, convert to block scalar // if value contains a colon, convert to block scalar
if (value.includes(":")) { if (value.includes(":")) {
result.push(`${key}: |`) result.push(`${key}: |-`)
result.push(` ${value}`) result.push(` ${value}`)
continue continue
} }
@@ -66,20 +68,23 @@ export namespace ConfigMarkdown {
} }
export async function parse(filePath: string) { export async function parse(filePath: string) {
const raw = await Bun.file(filePath).text() const template = await Bun.file(filePath).text()
const template = preprocessFrontmatter(raw)
try { try {
const md = matter(template) const md = matter(template)
return md return md
} catch (err) { } catch {
throw new FrontmatterError( try {
{ return matter(fallbackSanitization(template))
path: filePath, } catch (err) {
message: `${filePath}: Failed to parse YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`, throw new FrontmatterError(
}, {
{ cause: err }, 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 hidden?: boolean
follow?: boolean follow?: boolean
maxDepth?: number maxDepth?: number
signal?: AbortSignal
}) { }) {
input.signal?.throwIfAborted()
const args = [await filepath(), "--files", "--glob=!.git/*"] const args = [await filepath(), "--files", "--glob=!.git/*"]
if (input.follow !== false) args.push("--follow") if (input.follow !== false) args.push("--follow")
if (input.hidden !== false) args.push("--hidden") if (input.hidden !== false) args.push("--hidden")
@@ -235,6 +238,7 @@ export namespace Ripgrep {
stdout: "pipe", stdout: "pipe",
stderr: "ignore", stderr: "ignore",
maxBuffer: 1024 * 1024 * 20, maxBuffer: 1024 * 1024 * 20,
signal: input.signal,
}) })
const reader = proc.stdout.getReader() const reader = proc.stdout.getReader()
@@ -243,6 +247,8 @@ export namespace Ripgrep {
try { try {
while (true) { while (true) {
input.signal?.throwIfAborted()
const { done, value } = await reader.read() const { done, value } = await reader.read()
if (done) break if (done) break
@@ -261,11 +267,13 @@ export namespace Ripgrep {
reader.releaseLock() reader.releaseLock()
await proc.exited 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) 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 { interface Node {
path: string[] path: string[]
children: Node[] children: Node[]
-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_EXPERIMENTAL_DISABLE_COPY_ON_SELECT = truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT")
export const OPENCODE_ENABLE_EXA = export const OPENCODE_ENABLE_EXA =
truthy("OPENCODE_ENABLE_EXA") || OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_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_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_OUTPUT_TOKEN_MAX = number("OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX")
export const OPENCODE_EXPERIMENTAL_OXFMT = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_OXFMT") export const OPENCODE_EXPERIMENTAL_OXFMT = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_OXFMT")
+1 -1
View File
@@ -33,7 +33,7 @@ await Promise.all([
fs.mkdir(Global.Path.bin, { recursive: true }), fs.mkdir(Global.Path.bin, { recursive: true }),
]) ])
const CACHE_VERSION = "20" const CACHE_VERSION = "21"
const version = await Bun.file(path.join(Global.Path.cache, "version")) const version = await Bun.file(path.join(Global.Path.cache, "version"))
.text() .text()
+2 -5
View File
@@ -1,5 +1,4 @@
import path from "path" import path from "path"
import fs from "fs/promises"
import z from "zod" import z from "zod"
import { Global } from "../global" import { Global } from "../global"
@@ -65,16 +64,14 @@ export namespace McpAuth {
if (serverUrl) { if (serverUrl) {
entry.serverUrl = serverUrl entry.serverUrl = serverUrl
} }
await Bun.write(file, JSON.stringify({ ...data, [mcpName]: entry }, null, 2)) await Bun.write(file, JSON.stringify({ ...data, [mcpName]: entry }, null, 2), { mode: 0o600 })
await fs.chmod(file.name!, 0o600)
} }
export async function remove(mcpName: string): Promise<void> { export async function remove(mcpName: string): Promise<void> {
const file = Bun.file(filepath) const file = Bun.file(filepath)
const data = await all() const data = await all()
delete data[mcpName] delete data[mcpName]
await Bun.write(file, JSON.stringify(data, null, 2)) await Bun.write(file, JSON.stringify(data, null, 2), { mode: 0o600 })
await fs.chmod(file.name!, 0o600)
} }
export async function updateTokens(mcpName: string, tokens: Tokens, serverUrl?: string): Promise<void> { 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... // 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 base = baseURL ?? model.api.url
const claude = model.id.includes("claude") // const claude = model.id.includes("claude")
const url = iife(() => { // const url = iife(() => {
if (!claude) return base // if (!claude) return base
if (base.endsWith("/v1")) return base // if (base.endsWith("/v1")) return base
if (base.endsWith("/")) return `${base}v1` // if (base.endsWith("/")) return `${base}v1`
return `${base}/v1` // return `${base}/v1`
}) // })
model.api.url = url // model.api.url = url
model.api.npm = claude ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot" // model.api.npm = claude ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot"
model.api.npm = "@ai-sdk/github-copilot"
} }
} }
return { return {
baseURL,
apiKey: "", apiKey: "",
async fetch(request: RequestInfo | URL, init?: RequestInit) { async fetch(request: RequestInfo | URL, init?: RequestInit) {
const info = await getAuth() const info = await getAuth()
if (info.type !== "oauth") return fetch(request, init) if (info.type !== "oauth") return fetch(request, init)
const url = request instanceof URL ? request.href : request.toString()
const { isVision, isAgent } = iife(() => { const { isVision, isAgent } = iife(() => {
try { try {
const body = typeof init?.body === "string" ? JSON.parse(init.body) : init?.body const body = typeof init?.body === "string" ? JSON.parse(init.body) : init?.body
// Completions API // Completions API
if (body?.messages) { if (body?.messages && url.includes("completions")) {
const last = body.messages[body.messages.length - 1] const last = body.messages[body.messages.length - 1]
return { return {
isVision: body.messages.some( isVision: body.messages.some(
@@ -88,6 +92,28 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
isAgent: last?.role !== "user", 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 {} } catch {}
return { isVision: false, isAgent: false } return { isVision: false, isAgent: false }
}) })
+1 -1
View File
@@ -15,7 +15,7 @@ import { CopilotAuthPlugin } from "./copilot"
export namespace Plugin { export namespace Plugin {
const log = Log.create({ service: "plugin" }) const log = Log.create({ service: "plugin" })
const BUILTIN = ["opencode-anthropic-auth@0.0.12", "@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) // Built-in plugins that are directly imported (not installed from npm)
const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin] const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin]
+30 -7
View File
@@ -14,6 +14,10 @@ interface Context {
const context = Context.create<Context>("instance") const context = Context.create<Context>("instance")
const cache = new Map<string, Promise<Context>>() const cache = new Map<string, Promise<Context>>()
const disposal = {
all: undefined as Promise<void> | undefined,
}
export const Instance = { export const Instance = {
async provide<R>(input: { directory: string; init?: () => Promise<any>; fn: () => R }): Promise<R> { async provide<R>(input: { directory: string; init?: () => Promise<any>; fn: () => R }): Promise<R> {
let existing = cache.get(input.directory) let existing = cache.get(input.directory)
@@ -77,15 +81,34 @@ export const Instance = {
}) })
}, },
async disposeAll() { async disposeAll() {
Log.Default.info("disposing all instances") if (disposal.all) return disposal.all
for (const [_key, value] of cache) {
const awaited = await value.catch(() => {}) disposal.all = iife(async () => {
if (awaited) { Log.Default.info("disposing all instances")
await context.provide(await value, async () => { 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() await Instance.dispose()
}) })
} }
} }).finally(() => {
cache.clear() disposal.all = undefined
})
return disposal.all
}, },
} }
+7 -3
View File
@@ -46,20 +46,24 @@ export namespace State {
}, 10000).unref() }, 10000).unref()
const tasks: Promise<void>[] = [] const tasks: Promise<void>[] = []
for (const entry of entries.values()) { for (const [init, entry] of entries) {
if (!entry.dispose) continue if (!entry.dispose) continue
const label = typeof init === "function" ? init.name : String(init)
const task = Promise.resolve(entry.state) const task = Promise.resolve(entry.state)
.then((state) => entry.dispose!(state)) .then((state) => entry.dispose!(state))
.catch((error) => { .catch((error) => {
log.error("Error while disposing state:", { error, key }) log.error("Error while disposing state:", { error, key, init: label })
}) })
tasks.push(task) tasks.push(task)
} }
await Promise.all(tasks)
entries.clear() entries.clear()
recordsByKey.delete(key) recordsByKey.delete(key)
await Promise.all(tasks)
disposalFinished = true disposalFinished = true
log.info("state disposal completed", { key }) log.info("state disposal completed", { key })
} }
+1 -1
View File
@@ -977,7 +977,7 @@ export namespace Provider {
...model.headers, ...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) const existing = s.sdk.get(key)
if (existing) return existing if (existing) return existing
+10 -3
View File
@@ -319,7 +319,14 @@ export namespace ProviderTransform {
if (!model.capabilities.reasoning) return {} if (!model.capabilities.reasoning) return {}
const id = model.id.toLowerCase() 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 // see: https://docs.x.ai/docs/guides/reasoning#control-how-hard-the-model-thinks
if (id.includes("grok") && id.includes("grok-3-mini")) { if (id.includes("grok") && id.includes("grok-3-mini")) {
@@ -428,13 +435,13 @@ export namespace ProviderTransform {
high: { high: {
thinking: { thinking: {
type: "enabled", type: "enabled",
budgetTokens: 16000, budgetTokens: Math.min(16_000, Math.floor(model.limit.output / 2 - 1)),
}, },
}, },
max: { max: {
thinking: { thinking: {
type: "enabled", 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 { Hono } from "hono"
import { describeRoute, resolver } from "hono-openapi" import { describeRoute, resolver, validator } from "hono-openapi"
import { streamSSE } from "hono/streaming" import { streamSSE } from "hono/streaming"
import z from "zod" import z from "zod"
import { BusEvent } from "@/bus/bus-event" import { BusEvent } from "@/bus/bus-event"
@@ -8,6 +8,8 @@ import { Instance } from "../../project/instance"
import { Installation } from "@/installation" import { Installation } from "@/installation"
import { Log } from "../../util/log" import { Log } from "../../util/log"
import { lazy } from "../../util/lazy" import { lazy } from "../../util/lazy"
import { Config } from "../../config/config"
import { errors } from "../error"
const log = Log.create({ service: "server" }) 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( .post(
"/dispose", "/dispose",
describeRoute({ describeRoute({
+63 -63
View File
@@ -122,6 +122,68 @@ export namespace Server {
}), }),
) )
.route("/global", GlobalRoutes()) .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) => { .use(async (c, next) => {
let directory = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd() let directory = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
try { try {
@@ -409,68 +471,6 @@ export namespace Server {
return c.json(await Format.status()) 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( .get(
"/event", "/event",
describeRoute({ describeRoute({
@@ -539,7 +539,7 @@ export namespace Server {
}) })
response.headers.set( response.headers.set(
"Content-Security-Policy", "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 return response
}) as unknown as Hono, }) as unknown as Hono,
+14
View File
@@ -39,6 +39,16 @@ export namespace Session {
).test(title) ).test(title)
} }
function getForkedTitle(title: string): string {
const match = title.match(/^(.+) \(fork #(\d+)\)$/)
if (match) {
const base = match[1]
const num = parseInt(match[2], 10)
return `${base} (fork #${num + 1})`
}
return `${title} (fork #1)`
}
export const Info = z export const Info = z
.object({ .object({
id: Identifier.schema("session"), id: Identifier.schema("session"),
@@ -151,8 +161,12 @@ export namespace Session {
messageID: Identifier.schema("message").optional(), messageID: Identifier.schema("message").optional(),
}), }),
async (input) => { async (input) => {
const original = await get(input.sessionID)
if (!original) throw new Error("session not found")
const title = getForkedTitle(original.title)
const session = await createNext({ const session = await createNext({
directory: Instance.directory, directory: Instance.directory,
title,
}) })
const msgs = await messages({ sessionID: input.sessionID }) const msgs = await messages({ sessionID: input.sessionID })
const idMap = new Map<string, string>() const idMap = new Map<string, string>()
+8 -14
View File
@@ -150,20 +150,14 @@ export namespace LLM {
}, },
) )
const maxOutputTokens = isCodex ? undefined : undefined const maxOutputTokens = isCodex
log.info("max_output_tokens", { ? undefined
tokens: ProviderTransform.maxOutputTokens( : ProviderTransform.maxOutputTokens(
input.model.api.npm, input.model.api.npm,
params.options, params.options,
input.model.limit.output, input.model.limit.output,
OUTPUT_TOKEN_MAX, OUTPUT_TOKEN_MAX,
), )
modelOptions: params.options,
outputLimit: input.model.limit.output,
})
// tokens = 32000
// outputLimit = 64000
// modelOptions={"reasoningEffort":"minimal"}
const tools = await resolveTools(input) const tools = await resolveTools(input)
@@ -176,6 +176,8 @@ export namespace MessageV2 {
}) })
.optional(), .optional(),
command: z.string().optional(), command: z.string().optional(),
}).meta({
ref: "SubtaskPart",
}) })
export type SubtaskPart = z.infer<typeof SubtaskPart> export type SubtaskPart = z.infer<typeof SubtaskPart>
+1
View File
@@ -38,6 +38,7 @@ export const GlobTool = Tool.define("glob", {
for await (const file of Ripgrep.files({ for await (const file of Ripgrep.files({
cwd: search, cwd: search,
glob: [params.pattern], glob: [params.pattern],
signal: ctx.abort,
})) { })) {
if (files.length >= limit) { if (files.length >= limit) {
truncated = true truncated = true
+1
View File
@@ -54,6 +54,7 @@ export const GrepTool = Tool.define("grep", {
const proc = Bun.spawn([rgPath, ...args], { const proc = Bun.spawn([rgPath, ...args], {
stdout: "pipe", stdout: "pipe",
stderr: "pipe", stderr: "pipe",
signal: ctx.abort,
}) })
const output = await new Response(proc.stdout).text() const output = await new Response(proc.stdout).text()
+1 -1
View File
@@ -56,7 +56,7 @@ export const ListTool = Tool.define("list", {
const ignoreGlobs = IGNORE_PATTERNS.map((p) => `!${p}*`).concat(params.ignore?.map((p) => `!${p}`) || []) const ignoreGlobs = IGNORE_PATTERNS.map((p) => `!${p}*`).concat(params.ignore?.map((p) => `!${p}`) || [])
const files = [] const files = []
for await (const file of Ripgrep.files({ cwd: searchPath, glob: ignoreGlobs })) { for await (const file of Ripgrep.files({ cwd: searchPath, glob: ignoreGlobs, signal: ctx.abort })) {
files.push(file) files.push(file)
if (files.length >= LIMIT) break if (files.length >= LIMIT) break
} }
@@ -0,0 +1,11 @@
# Response Formatting Requirements
Always structure your responses using clear markdown formatting:
- By default don't put information into tables for questions (but do put information into tables when creating or updating files)
- Use headings (##, ###) to organise sections, always
- Use bullet points or numbered lists for multiple items
- Use code blocks with language tags for any code
- Use **bold** for key terms and emphasis
- Use tables when comparing options or listing structured data
- Break long responses into logical sections with headings
@@ -0,0 +1,13 @@
---
description: General coding and planning agent
mode: subagent
model: synthetic/hf:zai-org/GLM-4.7
tools:
write: true
read: true
edit: true
stuff: >
This is some stuff
---
Strictly follow da rules
+41 -5
View File
@@ -104,7 +104,7 @@ describe("ConfigMarkdown: frontmatter parsing", async () => {
}) })
test("should extract occupation field with colon in value", () => { test("should extract occupation field with colon in value", () => {
expect(parsed.data.occupation).toBe("This man has the following occupation: Software Engineer\n") expect(parsed.data.occupation).toBe("This man has the following occupation: Software Engineer")
}) })
test("should extract title field with single quotes", () => { test("should extract title field with single quotes", () => {
@@ -128,15 +128,15 @@ describe("ConfigMarkdown: frontmatter parsing", async () => {
}) })
test("should extract URL with port", () => { test("should extract URL with port", () => {
expect(parsed.data.url).toBe("https://example.com:8080/path?query=value\n") expect(parsed.data.url).toBe("https://example.com:8080/path?query=value")
}) })
test("should extract time with colons", () => { test("should extract time with colons", () => {
expect(parsed.data.time).toBe("The time is 12:30:00 PM\n") expect(parsed.data.time).toBe("The time is 12:30:00 PM")
}) })
test("should extract value with multiple colons", () => { test("should extract value with multiple colons", () => {
expect(parsed.data.nested).toBe("First: Second: Third: Fourth\n") expect(parsed.data.nested).toBe("First: Second: Third: Fourth")
}) })
test("should preserve already double-quoted values with colons", () => { test("should preserve already double-quoted values with colons", () => {
@@ -148,7 +148,7 @@ describe("ConfigMarkdown: frontmatter parsing", async () => {
}) })
test("should extract value with quotes and colons mixed", () => { test("should extract value with quotes and colons mixed", () => {
expect(parsed.data.mixed).toBe('He said "hello: world" and then left\n') expect(parsed.data.mixed).toBe('He said "hello: world" and then left')
}) })
test("should handle empty values", () => { test("should handle empty values", () => {
@@ -190,3 +190,39 @@ describe("ConfigMarkdown: frontmatter parsing w/ no frontmatter", async () => {
expect(result.content.trim()).toBe("Content") expect(result.content.trim()).toBe("Content")
}) })
}) })
describe("ConfigMarkdown: frontmatter parsing w/ Markdown header", async () => {
const result = await ConfigMarkdown.parse(import.meta.dir + "/fixtures/markdown-header.md")
test("should parse and match", () => {
expect(result).toBeDefined()
expect(result.data).toEqual({})
expect(result.content.trim()).toBe(`# Response Formatting Requirements
Always structure your responses using clear markdown formatting:
- By default don't put information into tables for questions (but do put information into tables when creating or updating files)
- Use headings (##, ###) to organise sections, always
- Use bullet points or numbered lists for multiple items
- Use code blocks with language tags for any code
- Use **bold** for key terms and emphasis
- Use tables when comparing options or listing structured data
- Break long responses into logical sections with headings`)
})
})
describe("ConfigMarkdown: frontmatter has weird model id", async () => {
const result = await ConfigMarkdown.parse(import.meta.dir + "/fixtures/weird-model-id.md")
test("should parse and match", () => {
expect(result).toBeDefined()
expect(result.data["description"]).toEqual("General coding and planning agent")
expect(result.data["mode"]).toEqual("subagent")
expect(result.data["model"]).toEqual("synthetic/hf:zai-org/GLM-4.7")
expect(result.data["tools"]["write"]).toBeTrue()
expect(result.data["tools"]["read"]).toBeTrue()
expect(result.data["stuff"]).toBe("This is some stuff\n")
expect(result.content.trim()).toBe("Strictly follow da rules")
})
})
@@ -1056,8 +1056,8 @@ describe("ProviderTransform.variants", () => {
cache: { read: 0.0001, write: 0.0002 }, cache: { read: 0.0001, write: 0.0002 },
}, },
limit: { limit: {
context: 128000, context: 200_000,
output: 8192, output: 64_000,
}, },
status: "active", status: "active",
options: {}, options: {},
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.1.38", "version": "1.1.42",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.1.38", "version": "1.1.42",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
+117 -84
View File
@@ -14,7 +14,7 @@ import type {
AuthSetErrors, AuthSetErrors,
AuthSetResponses, AuthSetResponses,
CommandListResponses, CommandListResponses,
Config as Config2, Config as Config3,
ConfigGetResponses, ConfigGetResponses,
ConfigProvidersResponses, ConfigProvidersResponses,
ConfigUpdateErrors, ConfigUpdateErrors,
@@ -34,6 +34,9 @@ import type {
FindSymbolsResponses, FindSymbolsResponses,
FindTextResponses, FindTextResponses,
FormatterStatusResponses, FormatterStatusResponses,
GlobalConfigGetResponses,
GlobalConfigUpdateErrors,
GlobalConfigUpdateResponses,
GlobalDisposeResponses, GlobalDisposeResponses,
GlobalEventResponses, GlobalEventResponses,
GlobalHealthResponses, GlobalHealthResponses,
@@ -215,6 +218,44 @@ class HeyApiRegistry<T> {
} }
} }
export class Config extends HeyApiClient {
/**
* Get global configuration
*
* Retrieve the current global OpenCode configuration settings and preferences.
*/
public get<ThrowOnError extends boolean = false>(options?: Options<never, ThrowOnError>) {
return (options?.client ?? this.client).get<GlobalConfigGetResponses, unknown, ThrowOnError>({
url: "/global/config",
...options,
})
}
/**
* Update global configuration
*
* Update global OpenCode configuration settings and preferences.
*/
public update<ThrowOnError extends boolean = false>(
parameters?: {
config?: Config3
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams([parameters], [{ args: [{ key: "config", map: "body" }] }])
return (options?.client ?? this.client).patch<GlobalConfigUpdateResponses, GlobalConfigUpdateErrors, ThrowOnError>({
url: "/global/config",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
}
export class Global extends HeyApiClient { export class Global extends HeyApiClient {
/** /**
* Get health * Get health
@@ -251,6 +292,67 @@ export class Global extends HeyApiClient {
...options, ...options,
}) })
} }
private _config?: Config
get config(): Config {
return (this._config ??= new Config({ client: this.client }))
}
}
export class Auth extends HeyApiClient {
/**
* Remove auth credentials
*
* Remove authentication credentials
*/
public remove<ThrowOnError extends boolean = false>(
parameters: {
providerID: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "providerID" }] }])
return (options?.client ?? this.client).delete<AuthRemoveResponses, AuthRemoveErrors, ThrowOnError>({
url: "/auth/{providerID}",
...options,
...params,
})
}
/**
* Set auth credentials
*
* Set authentication credentials
*/
public set<ThrowOnError extends boolean = false>(
parameters: {
providerID: string
auth?: Auth3
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "providerID" },
{ key: "auth", map: "body" },
],
},
],
)
return (options?.client ?? this.client).put<AuthSetResponses, AuthSetErrors, ThrowOnError>({
url: "/auth/{providerID}",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
} }
export class Project extends HeyApiClient { export class Project extends HeyApiClient {
@@ -541,7 +643,7 @@ export class Pty extends HeyApiClient {
} }
} }
export class Config extends HeyApiClient { export class Config2 extends HeyApiClient {
/** /**
* Get configuration * Get configuration
* *
@@ -569,7 +671,7 @@ export class Config extends HeyApiClient {
public update<ThrowOnError extends boolean = false>( public update<ThrowOnError extends boolean = false>(
parameters?: { parameters?: {
directory?: string directory?: string
config?: Config2 config?: Config3
}, },
options?: Options<never, ThrowOnError>, options?: Options<never, ThrowOnError>,
) { ) {
@@ -2238,7 +2340,7 @@ export class File extends HeyApiClient {
} }
} }
export class Auth extends HeyApiClient { export class Auth2 extends HeyApiClient {
/** /**
* Remove MCP OAuth * Remove MCP OAuth
* *
@@ -2482,9 +2584,9 @@ export class Mcp extends HeyApiClient {
}) })
} }
private _auth?: Auth private _auth?: Auth2
get auth(): Auth { get auth(): Auth2 {
return (this._auth ??= new Auth({ client: this.client })) return (this._auth ??= new Auth2({ client: this.client }))
} }
} }
@@ -3055,75 +3157,6 @@ export class Formatter extends HeyApiClient {
} }
} }
export class Auth2 extends HeyApiClient {
/**
* Remove auth credentials
*
* Remove authentication credentials
*/
public remove<ThrowOnError extends boolean = false>(
parameters: {
providerID: string
directory?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "providerID" },
{ in: "query", key: "directory" },
],
},
],
)
return (options?.client ?? this.client).delete<AuthRemoveResponses, AuthRemoveErrors, ThrowOnError>({
url: "/auth/{providerID}",
...options,
...params,
})
}
/**
* Set auth credentials
*
* Set authentication credentials
*/
public set<ThrowOnError extends boolean = false>(
parameters: {
providerID: string
directory?: string
auth?: Auth3
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "providerID" },
{ in: "query", key: "directory" },
{ key: "auth", map: "body" },
],
},
],
)
return (options?.client ?? this.client).put<AuthSetResponses, AuthSetErrors, ThrowOnError>({
url: "/auth/{providerID}",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
}
export class Event extends HeyApiClient { export class Event extends HeyApiClient {
/** /**
* Subscribe to events * Subscribe to events
@@ -3158,6 +3191,11 @@ export class OpencodeClient extends HeyApiClient {
return (this._global ??= new Global({ client: this.client })) return (this._global ??= new Global({ client: this.client }))
} }
private _auth?: Auth
get auth(): Auth {
return (this._auth ??= new Auth({ client: this.client }))
}
private _project?: Project private _project?: Project
get project(): Project { get project(): Project {
return (this._project ??= new Project({ client: this.client })) return (this._project ??= new Project({ client: this.client }))
@@ -3168,9 +3206,9 @@ export class OpencodeClient extends HeyApiClient {
return (this._pty ??= new Pty({ client: this.client })) return (this._pty ??= new Pty({ client: this.client }))
} }
private _config?: Config private _config?: Config2
get config(): Config { get config(): Config2 {
return (this._config ??= new Config({ client: this.client })) return (this._config ??= new Config2({ client: this.client }))
} }
private _tool?: Tool private _tool?: Tool
@@ -3268,11 +3306,6 @@ export class OpencodeClient extends HeyApiClient {
return (this._formatter ??= new Formatter({ client: this.client })) return (this._formatter ??= new Formatter({ client: this.client }))
} }
private _auth?: Auth2
get auth(): Auth2 {
return (this._auth ??= new Auth2({ client: this.client }))
}
private _event?: Event private _event?: Event
get event(): Event { get event(): Event {
return (this._event ??= new Event({ client: this.client })) return (this._event ??= new Event({ client: this.client }))
+148 -109
View File
@@ -233,6 +233,21 @@ export type TextPart = {
} }
} }
export type SubtaskPart = {
id: string
sessionID: string
messageID: string
type: "subtask"
prompt: string
description: string
agent: string
model?: {
providerID: string
modelID: string
}
command?: string
}
export type ReasoningPart = { export type ReasoningPart = {
id: string id: string
sessionID: string sessionID: string
@@ -449,20 +464,7 @@ export type CompactionPart = {
export type Part = export type Part =
| TextPart | TextPart
| { | SubtaskPart
id: string
sessionID: string
messageID: string
type: "subtask"
prompt: string
description: string
agent: string
model?: {
providerID: string
modelID: string
}
command?: string
}
| ReasoningPart | ReasoningPart
| FilePart | FilePart
| ToolPart | ToolPart
@@ -930,21 +932,6 @@ export type GlobalEvent = {
payload: Event payload: Event
} }
export type BadRequestError = {
data: unknown
errors: Array<{
[key: string]: unknown
}>
success: false
}
export type NotFoundError = {
name: "NotFoundError"
data: {
message: string
}
}
/** /**
* Custom keybind configurations * Custom keybind configurations
*/ */
@@ -1826,6 +1813,43 @@ export type Config = {
} }
} }
export type BadRequestError = {
data: unknown
errors: Array<{
[key: string]: unknown
}>
success: false
}
export type OAuth = {
type: "oauth"
refresh: string
access: string
expires: number
accountId?: string
enterpriseUrl?: string
}
export type ApiAuth = {
type: "api"
key: string
}
export type WellKnownAuth = {
type: "wellknown"
key: string
token: string
}
export type Auth = OAuth | ApiAuth | WellKnownAuth
export type NotFoundError = {
name: "NotFoundError"
data: {
message: string
}
}
export type Model = { export type Model = {
id: string id: string
providerID: string providerID: string
@@ -2142,28 +2166,6 @@ export type FormatterStatus = {
enabled: boolean enabled: boolean
} }
export type OAuth = {
type: "oauth"
refresh: string
access: string
expires: number
accountId?: string
enterpriseUrl?: string
}
export type ApiAuth = {
type: "api"
key: string
}
export type WellKnownAuth = {
type: "wellknown"
key: string
token: string
}
export type Auth = OAuth | ApiAuth | WellKnownAuth
export type GlobalHealthData = { export type GlobalHealthData = {
body?: never body?: never
path?: never path?: never
@@ -2199,6 +2201,47 @@ export type GlobalEventResponses = {
export type GlobalEventResponse = GlobalEventResponses[keyof GlobalEventResponses] export type GlobalEventResponse = GlobalEventResponses[keyof GlobalEventResponses]
export type GlobalConfigGetData = {
body?: never
path?: never
query?: never
url: "/global/config"
}
export type GlobalConfigGetResponses = {
/**
* Get global config info
*/
200: Config
}
export type GlobalConfigGetResponse = GlobalConfigGetResponses[keyof GlobalConfigGetResponses]
export type GlobalConfigUpdateData = {
body?: Config
path?: never
query?: never
url: "/global/config"
}
export type GlobalConfigUpdateErrors = {
/**
* Bad request
*/
400: BadRequestError
}
export type GlobalConfigUpdateError = GlobalConfigUpdateErrors[keyof GlobalConfigUpdateErrors]
export type GlobalConfigUpdateResponses = {
/**
* Successfully updated global config
*/
200: Config
}
export type GlobalConfigUpdateResponse = GlobalConfigUpdateResponses[keyof GlobalConfigUpdateResponses]
export type GlobalDisposeData = { export type GlobalDisposeData = {
body?: never body?: never
path?: never path?: never
@@ -2215,6 +2258,60 @@ export type GlobalDisposeResponses = {
export type GlobalDisposeResponse = GlobalDisposeResponses[keyof GlobalDisposeResponses] export type GlobalDisposeResponse = GlobalDisposeResponses[keyof GlobalDisposeResponses]
export type AuthRemoveData = {
body?: never
path: {
providerID: string
}
query?: never
url: "/auth/{providerID}"
}
export type AuthRemoveErrors = {
/**
* Bad request
*/
400: BadRequestError
}
export type AuthRemoveError = AuthRemoveErrors[keyof AuthRemoveErrors]
export type AuthRemoveResponses = {
/**
* Successfully removed authentication credentials
*/
200: boolean
}
export type AuthRemoveResponse = AuthRemoveResponses[keyof AuthRemoveResponses]
export type AuthSetData = {
body?: Auth
path: {
providerID: string
}
query?: never
url: "/auth/{providerID}"
}
export type AuthSetErrors = {
/**
* Bad request
*/
400: BadRequestError
}
export type AuthSetError = AuthSetErrors[keyof AuthSetErrors]
export type AuthSetResponses = {
/**
* Successfully set authentication credentials
*/
200: boolean
}
export type AuthSetResponse = AuthSetResponses[keyof AuthSetResponses]
export type ProjectListData = { export type ProjectListData = {
body?: never body?: never
path?: never path?: never
@@ -4867,64 +4964,6 @@ export type FormatterStatusResponses = {
export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses] export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses]
export type AuthRemoveData = {
body?: never
path: {
providerID: string
}
query?: {
directory?: string
}
url: "/auth/{providerID}"
}
export type AuthRemoveErrors = {
/**
* Bad request
*/
400: BadRequestError
}
export type AuthRemoveError = AuthRemoveErrors[keyof AuthRemoveErrors]
export type AuthRemoveResponses = {
/**
* Successfully removed authentication credentials
*/
200: boolean
}
export type AuthRemoveResponse = AuthRemoveResponses[keyof AuthRemoveResponses]
export type AuthSetData = {
body?: Auth
path: {
providerID: string
}
query?: {
directory?: string
}
url: "/auth/{providerID}"
}
export type AuthSetErrors = {
/**
* Bad request
*/
400: BadRequestError
}
export type AuthSetError = AuthSetErrors[keyof AuthSetErrors]
export type AuthSetResponses = {
/**
* Successfully set authentication credentials
*/
200: boolean
}
export type AuthSetResponse = AuthSetResponses[keyof AuthSetResponses]
export type EventSubscribeData = { export type EventSubscribeData = {
body?: never body?: never
path?: never path?: never
+315 -259
View File
@@ -66,6 +66,73 @@
] ]
} }
}, },
"/global/config": {
"get": {
"operationId": "global.config.get",
"summary": "Get global configuration",
"description": "Retrieve the current global OpenCode configuration settings and preferences.",
"responses": {
"200": {
"description": "Get global config info",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Config"
}
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.config.get({\n ...\n})"
}
]
},
"patch": {
"operationId": "global.config.update",
"summary": "Update global configuration",
"description": "Update global OpenCode configuration settings and preferences.",
"responses": {
"200": {
"description": "Successfully updated global config",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Config"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Config"
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.config.update({\n ...\n})"
}
]
}
},
"/global/dispose": { "/global/dispose": {
"post": { "post": {
"operationId": "global.dispose", "operationId": "global.dispose",
@@ -91,6 +158,103 @@
] ]
} }
}, },
"/auth/{providerID}": {
"put": {
"operationId": "auth.set",
"summary": "Set auth credentials",
"description": "Set authentication credentials",
"responses": {
"200": {
"description": "Successfully set authentication credentials",
"content": {
"application/json": {
"schema": {
"type": "boolean"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
}
},
"parameters": [
{
"in": "path",
"name": "providerID",
"schema": {
"type": "string"
},
"required": true
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Auth"
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.set({\n ...\n})"
}
]
},
"delete": {
"operationId": "auth.remove",
"summary": "Remove auth credentials",
"description": "Remove authentication credentials",
"responses": {
"200": {
"description": "Successfully removed authentication credentials",
"content": {
"application/json": {
"schema": {
"type": "boolean"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
}
},
"parameters": [
{
"in": "path",
"name": "providerID",
"schema": {
"type": "string"
},
"required": true
}
],
"x-codeSamples": [
{
"lang": "js",
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.remove({\n ...\n})"
}
]
}
},
"/project": { "/project": {
"get": { "get": {
"operationId": "project.list", "operationId": "project.list",
@@ -5650,117 +5814,6 @@
] ]
} }
}, },
"/auth/{providerID}": {
"put": {
"operationId": "auth.set",
"parameters": [
{
"in": "query",
"name": "directory",
"schema": {
"type": "string"
}
},
{
"in": "path",
"name": "providerID",
"schema": {
"type": "string"
},
"required": true
}
],
"summary": "Set auth credentials",
"description": "Set authentication credentials",
"responses": {
"200": {
"description": "Successfully set authentication credentials",
"content": {
"application/json": {
"schema": {
"type": "boolean"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Auth"
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.set({\n ...\n})"
}
]
},
"delete": {
"operationId": "auth.remove",
"parameters": [
{
"in": "query",
"name": "directory",
"schema": {
"type": "string"
}
},
{
"in": "path",
"name": "providerID",
"schema": {
"type": "string"
},
"required": true
}
],
"summary": "Remove auth credentials",
"description": "Remove authentication credentials",
"responses": {
"200": {
"description": "Successfully removed authentication credentials",
"content": {
"application/json": {
"schema": {
"type": "boolean"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.remove({\n ...\n})"
}
]
}
},
"/event": { "/event": {
"get": { "get": {
"operationId": "event.subscribe", "operationId": "event.subscribe",
@@ -6449,6 +6502,49 @@
}, },
"required": ["id", "sessionID", "messageID", "type", "text"] "required": ["id", "sessionID", "messageID", "type", "text"]
}, },
"SubtaskPart": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"sessionID": {
"type": "string"
},
"messageID": {
"type": "string"
},
"type": {
"type": "string",
"const": "subtask"
},
"prompt": {
"type": "string"
},
"description": {
"type": "string"
},
"agent": {
"type": "string"
},
"model": {
"type": "object",
"properties": {
"providerID": {
"type": "string"
},
"modelID": {
"type": "string"
}
},
"required": ["providerID", "modelID"]
},
"command": {
"type": "string"
}
},
"required": ["id", "sessionID", "messageID", "type", "prompt", "description", "agent"]
},
"ReasoningPart": { "ReasoningPart": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -7072,47 +7168,7 @@
"$ref": "#/components/schemas/TextPart" "$ref": "#/components/schemas/TextPart"
}, },
{ {
"type": "object", "$ref": "#/components/schemas/SubtaskPart"
"properties": {
"id": {
"type": "string"
},
"sessionID": {
"type": "string"
},
"messageID": {
"type": "string"
},
"type": {
"type": "string",
"const": "subtask"
},
"prompt": {
"type": "string"
},
"description": {
"type": "string"
},
"agent": {
"type": "string"
},
"model": {
"type": "object",
"properties": {
"providerID": {
"type": "string"
},
"modelID": {
"type": "string"
}
},
"required": ["providerID", "modelID"]
},
"command": {
"type": "string"
}
},
"required": ["id", "sessionID", "messageID", "type", "prompt", "description", "agent"]
}, },
{ {
"$ref": "#/components/schemas/ReasoningPart" "$ref": "#/components/schemas/ReasoningPart"
@@ -8352,46 +8408,6 @@
}, },
"required": ["directory", "payload"] "required": ["directory", "payload"]
}, },
"BadRequestError": {
"type": "object",
"properties": {
"data": {},
"errors": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
},
"success": {
"type": "boolean",
"const": false
}
},
"required": ["data", "errors", "success"]
},
"NotFoundError": {
"type": "object",
"properties": {
"name": {
"type": "string",
"const": "NotFoundError"
},
"data": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
},
"required": ["message"]
}
},
"required": ["name", "data"]
},
"KeybindsConfig": { "KeybindsConfig": {
"description": "Custom keybind configurations", "description": "Custom keybind configurations",
"type": "object", "type": "object",
@@ -9898,6 +9914,113 @@
}, },
"additionalProperties": false "additionalProperties": false
}, },
"BadRequestError": {
"type": "object",
"properties": {
"data": {},
"errors": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
},
"success": {
"type": "boolean",
"const": false
}
},
"required": ["data", "errors", "success"]
},
"OAuth": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "oauth"
},
"refresh": {
"type": "string"
},
"access": {
"type": "string"
},
"expires": {
"type": "number"
},
"accountId": {
"type": "string"
},
"enterpriseUrl": {
"type": "string"
}
},
"required": ["type", "refresh", "access", "expires"]
},
"ApiAuth": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "api"
},
"key": {
"type": "string"
}
},
"required": ["type", "key"]
},
"WellKnownAuth": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "wellknown"
},
"key": {
"type": "string"
},
"token": {
"type": "string"
}
},
"required": ["type", "key", "token"]
},
"Auth": {
"anyOf": [
{
"$ref": "#/components/schemas/OAuth"
},
{
"$ref": "#/components/schemas/ApiAuth"
},
{
"$ref": "#/components/schemas/WellKnownAuth"
}
]
},
"NotFoundError": {
"type": "object",
"properties": {
"name": {
"type": "string",
"const": "NotFoundError"
},
"data": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
},
"required": ["message"]
}
},
"required": ["name", "data"]
},
"Model": { "Model": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -10824,73 +10947,6 @@
} }
}, },
"required": ["name", "extensions", "enabled"] "required": ["name", "extensions", "enabled"]
},
"OAuth": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "oauth"
},
"refresh": {
"type": "string"
},
"access": {
"type": "string"
},
"expires": {
"type": "number"
},
"accountId": {
"type": "string"
},
"enterpriseUrl": {
"type": "string"
}
},
"required": ["type", "refresh", "access", "expires"]
},
"ApiAuth": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "api"
},
"key": {
"type": "string"
}
},
"required": ["type", "key"]
},
"WellKnownAuth": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "wellknown"
},
"key": {
"type": "string"
},
"token": {
"type": "string"
}
},
"required": ["type", "key", "token"]
},
"Auth": {
"anyOf": [
{
"$ref": "#/components/schemas/OAuth"
},
{
"$ref": "#/components/schemas/ApiAuth"
},
{
"$ref": "#/components/schemas/WellKnownAuth"
}
]
} }
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.1.38", "version": "1.1.42",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.1.38", "version": "1.1.42",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"exports": { "exports": {
+3 -1
View File
@@ -60,7 +60,9 @@ export function Dialog(props: DialogProps) {
</div> </div>
</Show> </Show>
<Show when={props.description}> <Show when={props.description}>
<Kobalte.Description data-slot="dialog-description">{props.description}</Kobalte.Description> <Kobalte.Description data-slot="dialog-description" style={{ "margin-left": "-4px" }}>
{props.description}
</Kobalte.Description>
</Show> </Show>
<div data-slot="dialog-body">{props.children}</div> <div data-slot="dialog-body">{props.children}</div>
</Kobalte.Content> </Kobalte.Content>
+1 -1
View File
@@ -187,7 +187,7 @@
[data-slot="list-header"] { [data-slot="list-header"] {
display: flex; display: flex;
z-index: 10; z-index: 10;
padding: 8px 12px 8px 12px; padding: 8px 12px 8px 8px;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
align-self: stretch; align-self: stretch;
+3 -1
View File
@@ -390,12 +390,14 @@ export function SessionTurn(
const interval = Interval.fromDateTimes(from, to) const interval = Interval.fromDateTimes(from, to)
const unit: DurationUnit[] = interval.length("seconds") > 60 ? ["minutes", "seconds"] : ["seconds"] const unit: DurationUnit[] = interval.length("seconds") > 60 ? ["minutes", "seconds"] : ["seconds"]
return interval.toDuration(unit).normalize().reconfigure({ locale: i18n.locale() }).toHuman({ const locale = i18n.locale()
const human = interval.toDuration(unit).normalize().reconfigure({ locale }).toHuman({
notation: "compact", notation: "compact",
unitDisplay: "narrow", unitDisplay: "narrow",
compactDisplay: "short", compactDisplay: "short",
showZeros: false, showZeros: false,
}) })
return locale.startsWith("zh") ? human.replaceAll("、", "") : human
} }
const autoScroll = createAutoScroll({ const autoScroll = createAutoScroll({
+7 -2
View File
@@ -375,8 +375,13 @@
&[data-variant="settings"] { &[data-variant="settings"] {
[data-slot="tabs-list"] { [data-slot="tabs-list"] {
width: 200px; width: 150px;
min-width: 200px; min-width: 150px;
@media (min-width: 640px) {
width: 200px;
min-width: 200px;
}
padding: 12px; padding: 12px;
gap: 0; gap: 0;
background-color: var(--background-base); background-color: var(--background-base);
+1
View File
@@ -475,6 +475,7 @@ export const { use: useMarked, provider: MarkedProvider } = createSimpleContext(
}, },
markedKatex({ markedKatex({
throwOnError: false, throwOnError: false,
nonStandard: true,
}), }),
markedShiki({ markedShiki({
async highlight(code, lang) { async highlight(code, lang) {
+102
View File
@@ -0,0 +1,102 @@
export const dict = {
"ui.sessionReview.title": "การเปลี่ยนแปลงเซสชัน",
"ui.sessionReview.diffStyle.unified": "แบบรวม",
"ui.sessionReview.diffStyle.split": "แบบแยก",
"ui.sessionReview.expandAll": "ขยายทั้งหมด",
"ui.sessionReview.collapseAll": "ย่อทั้งหมด",
"ui.sessionReview.change.added": "เพิ่ม",
"ui.sessionReview.change.removed": "ลบ",
"ui.lineComment.label.prefix": "แสดงความคิดเห็นบน ",
"ui.lineComment.label.suffix": "",
"ui.lineComment.editorLabel.prefix": "กำลังแสดงความคิดเห็นบน ",
"ui.lineComment.editorLabel.suffix": "",
"ui.lineComment.placeholder": "เพิ่มความคิดเห็น",
"ui.lineComment.submit": "แสดงความคิดเห็น",
"ui.sessionTurn.steps.show": "แสดงขั้นตอน",
"ui.sessionTurn.steps.hide": "ซ่อนขั้นตอน",
"ui.sessionTurn.summary.response": "การตอบสนอง",
"ui.sessionTurn.diff.showMore": "แสดงการเปลี่ยนแปลงเพิ่มเติม ({{count}})",
"ui.sessionTurn.retry.retrying": "กำลังลองใหม่",
"ui.sessionTurn.retry.inSeconds": "ใน {{seconds}}วิ",
"ui.sessionTurn.status.delegating": "มอบหมายงาน",
"ui.sessionTurn.status.planning": "วางแผนขั้นตอนถัดไป",
"ui.sessionTurn.status.gatheringContext": "รวบรวมบริบท",
"ui.sessionTurn.status.searchingCodebase": "กำลังค้นหาโค้ดเบส",
"ui.sessionTurn.status.searchingWeb": "กำลังค้นหาบนเว็บ",
"ui.sessionTurn.status.makingEdits": "กำลังแก้ไข",
"ui.sessionTurn.status.runningCommands": "กำลังเรียกใช้คำสั่ง",
"ui.sessionTurn.status.thinking": "กำลังคิด",
"ui.sessionTurn.status.thinkingWithTopic": "กำลังคิด - {{topic}}",
"ui.sessionTurn.status.gatheringThoughts": "รวบรวมความคิด",
"ui.sessionTurn.status.consideringNextSteps": "พิจารณาขั้นตอนถัดไป",
"ui.messagePart.diagnostic.error": "ข้อผิดพลาด",
"ui.messagePart.title.edit": "แก้ไข",
"ui.messagePart.title.write": "เขียน",
"ui.messagePart.option.typeOwnAnswer": "พิมพ์คำตอบของคุณเอง",
"ui.messagePart.review.title": "ตรวจสอบคำตอบของคุณ",
"ui.list.loading": "กำลังโหลด",
"ui.list.empty": "ไม่มีผลลัพธ์",
"ui.list.clearFilter": "ล้างตัวกรอง",
"ui.list.emptyWithFilter.prefix": "ไม่มีผลลัพธ์สำหรับ",
"ui.list.emptyWithFilter.suffix": "",
"ui.messageNav.newMessage": "ข้อความใหม่",
"ui.textField.copyToClipboard": "คัดลอกไปยังคลิปบอร์ด",
"ui.textField.copyLink": "คัดลอกลิงก์",
"ui.textField.copied": "คัดลอกแล้ว",
"ui.imagePreview.alt": "ตัวอย่างรูปภาพ",
"ui.tool.read": "อ่าน",
"ui.tool.list": "รายการ",
"ui.tool.glob": "Glob",
"ui.tool.grep": "Grep",
"ui.tool.webfetch": "ดึงจากเว็บ",
"ui.tool.shell": "เชลล์",
"ui.tool.patch": "แพตช์",
"ui.tool.todos": "รายการงาน",
"ui.tool.todos.read": "อ่านรายการงาน",
"ui.tool.questions": "คำถาม",
"ui.tool.agent": "เอเจนต์ {{type}}",
"ui.common.file.one": "ไฟล์",
"ui.common.file.other": "ไฟล์",
"ui.common.question.one": "คำถาม",
"ui.common.question.other": "คำถาม",
"ui.common.add": "เพิ่ม",
"ui.common.cancel": "ยกเลิก",
"ui.common.confirm": "ยืนยัน",
"ui.common.dismiss": "ปิด",
"ui.common.close": "ปิด",
"ui.common.next": "ถัดไป",
"ui.common.submit": "ส่ง",
"ui.permission.deny": "ปฏิเสธ",
"ui.permission.allowAlways": "อนุญาตเสมอ",
"ui.permission.allowOnce": "อนุญาตครั้งเดียว",
"ui.message.expand": "ขยายข้อความ",
"ui.message.collapse": "ย่อข้อความ",
"ui.message.copy": "คัดลอก",
"ui.message.copied": "คัดลอกแล้ว!",
"ui.message.attachment.alt": "ไฟล์แนบ",
"ui.patch.action.deleted": "ลบ",
"ui.patch.action.created": "สร้าง",
"ui.patch.action.moved": "ย้าย",
"ui.patch.action.patched": "แพตช์",
"ui.question.subtitle.answered": "{{count}} ตอบแล้ว",
"ui.question.answer.none": "(ไม่มีคำตอบ)",
"ui.question.review.notAnswered": "(ไม่ได้ตอบ)",
"ui.question.multiHint": "(เลือกทั้งหมดที่ใช้)",
"ui.question.custom.placeholder": "พิมพ์คำตอบของคุณ...",
}
+2 -2
View File
@@ -20,7 +20,7 @@ export const dict = {
"ui.sessionTurn.steps.show": "显示步骤", "ui.sessionTurn.steps.show": "显示步骤",
"ui.sessionTurn.steps.hide": "隐藏步骤", "ui.sessionTurn.steps.hide": "隐藏步骤",
"ui.sessionTurn.summary.response": "回复", "ui.sessionTurn.summary.response": "回复",
"ui.sessionTurn.diff.showMore": "显示更多更改 ({{count}})", "ui.sessionTurn.diff.showMore": "显示更多更改{{count}}",
"ui.sessionTurn.retry.retrying": "重试中", "ui.sessionTurn.retry.retrying": "重试中",
"ui.sessionTurn.retry.inSeconds": "{{seconds}} 秒后", "ui.sessionTurn.retry.inSeconds": "{{seconds}} 秒后",
@@ -33,7 +33,7 @@ export const dict = {
"ui.sessionTurn.status.makingEdits": "正在修改", "ui.sessionTurn.status.makingEdits": "正在修改",
"ui.sessionTurn.status.runningCommands": "正在运行命令", "ui.sessionTurn.status.runningCommands": "正在运行命令",
"ui.sessionTurn.status.thinking": "思考中", "ui.sessionTurn.status.thinking": "思考中",
"ui.sessionTurn.status.thinkingWithTopic": "思考 - {{topic}}", "ui.sessionTurn.status.thinkingWithTopic": "思考{{topic}}",
"ui.sessionTurn.status.gatheringThoughts": "正在整理思路", "ui.sessionTurn.status.gatheringThoughts": "正在整理思路",
"ui.sessionTurn.status.consideringNextSteps": "正在考虑下一步", "ui.sessionTurn.status.consideringNextSteps": "正在考虑下一步",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/util", "name": "@opencode-ai/util",
"version": "1.1.38", "version": "1.1.42",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"version": "1.1.38", "version": "1.1.42",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
+5 -1
View File
@@ -585,9 +585,13 @@ These environment variables enable experimental features that may change or be r
| `OPENCODE_EXPERIMENTAL` | boolean | Enable all experimental features | | `OPENCODE_EXPERIMENTAL` | boolean | Enable all experimental features |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Enable icon discovery | | `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Enable icon discovery |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Disable copy on select in TUI | | `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Disable copy on select in TUI |
| `OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH` | number | Max output length for bash commands |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Default timeout for bash commands in ms | | `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Default timeout for bash commands in ms |
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | Max output tokens for LLM responses | | `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | Max output tokens for LLM responses |
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Enable file watcher for entire dir | | `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Enable file watcher for entire dir |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Enable oxfmt formatter | | `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Enable oxfmt formatter |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Enable experimental LSP tool | | `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Enable experimental LSP tool |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Disable file watcher |
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Enable experimental Exa features |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Enable experimental LSP type checking |
| `OPENCODE_EXPERIMENTAL_MARKDOWN` | boolean | Enable experimental markdown features |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Enable plan mode |
+1
View File
@@ -15,6 +15,7 @@ export const team = [
"adamdotdevin", "adamdotdevin",
"iamdavidhill", "iamdavidhill",
"opencode-agent[bot]", "opencode-agent[bot]",
"R44VC0RP",
] ]
export async function getLatestRelease() { export async function getLatestRelease() {
+2 -2
View File
@@ -6,7 +6,7 @@ import { buildNotes, getLatestRelease } from "./changelog"
const highlightsTemplate = `## Highlights const highlightsTemplate = `## Highlights
<!-- <!--
Add highlights before publishing. Delete this section if no highlights. Add highlights before publishing. Delete this section if no highlights.
- For multiple highlights, use multiple <highlight> tags - For multiple highlights, use multiple <highlight> tags
@@ -40,7 +40,7 @@ console.log("=== publishing ===\n")
if (!Script.preview) { if (!Script.preview) {
const previous = await getLatestRelease() const previous = await getLatestRelease()
notes = await buildNotes(previous, "HEAD") notes = await buildNotes(previous, "HEAD")
notes.unshift(highlightsTemplate) // notes.unshift(highlightsTemplate)
} }
const pkgjsons = await Array.fromAsync( const pkgjsons = await Array.fromAsync(
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode", "name": "opencode",
"displayName": "opencode", "displayName": "opencode",
"description": "opencode for VS Code", "description": "opencode for VS Code",
"version": "1.1.38", "version": "1.1.42",
"publisher": "sst-dev", "publisher": "sst-dev",
"repository": { "repository": {
"type": "git", "type": "git",