diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 431581f596..8d4c9038a7 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -41,6 +41,13 @@ jobs:
- uses: ./.github/actions/setup-bun
+ - name: Setup git committer
+ id: committer
+ uses: ./.github/actions/setup-git-committer
+ with:
+ opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
+ opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
+
- name: Install OpenCode
if: inputs.bump || inputs.version
run: bun i -g opencode-ai
@@ -49,14 +56,16 @@ jobs:
run: |
./script/version.ts
env:
- GH_TOKEN: ${{ github.token }}
+ GH_TOKEN: ${{ steps.committer.outputs.token }}
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_VERSION: ${{ inputs.version }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
+ GH_REPO: ${{ (github.ref_name == 'beta' && 'anomalyco/opencode-beta') || github.repository }}
outputs:
version: ${{ steps.version.outputs.version }}
release: ${{ steps.version.outputs.release }}
tag: ${{ steps.version.outputs.tag }}
+ repo: ${{ steps.version.outputs.repo }}
build-cli:
needs: version
@@ -69,6 +78,13 @@ jobs:
- uses: ./.github/actions/setup-bun
+ - name: Setup git committer
+ id: committer
+ uses: ./.github/actions/setup-git-committer
+ with:
+ opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
+ opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
+
- name: Build
id: build
run: |
@@ -76,7 +92,8 @@ jobs:
env:
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
- GH_TOKEN: ${{ github.token }}
+ GH_REPO: ${{ needs.version.outputs.repo }}
+ GH_TOKEN: ${{ steps.committer.outputs.token }}
- uses: actions/upload-artifact@v4
with:
@@ -189,6 +206,13 @@ jobs:
if: contains(matrix.settings.host, 'ubuntu')
run: cargo tauri --version
+ - name: Setup git committer
+ id: committer
+ uses: ./.github/actions/setup-git-committer
+ with:
+ opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
+ opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
+
- name: Build and upload artifacts
uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
timeout-minutes: 60
@@ -196,14 +220,16 @@ jobs:
projectPath: packages/desktop
uploadWorkflowArtifacts: true
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
- args: --target ${{ matrix.settings.target }} --config ./src-tauri/tauri.prod.conf.json --verbose
+ args: --target ${{ matrix.settings.target }} --config ${{ (github.ref_name == 'beta' && './src-tauri/tauri.beta.conf.json') || './src-tauri/tauri.prod.conf.json' }} --verbose
updaterJsonPreferNsis: true
releaseId: ${{ needs.version.outputs.release }}
tagName: ${{ needs.version.outputs.tag }}
releaseDraft: true
releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
+ repo: ${{ (github.ref_name == 'beta' && 'opencode-beta') || '' }}
+ releaseCommitish: ${{ github.sha }}
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: true
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
@@ -280,4 +306,5 @@ jobs:
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
AUR_KEY: ${{ secrets.AUR_KEY }}
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
+ GH_REPO: ${{ needs.version.outputs.repo }}
NPM_CONFIG_PROVENANCE: false
diff --git a/.opencode/agent/translator.md b/.opencode/agent/translator.md
index 7886cf5f39..0c2e176d8b 100644
--- a/.opencode/agent/translator.md
+++ b/.opencode/agent/translator.md
@@ -1,7 +1,7 @@
---
description: Translate content for a specified locale while preserving technical terms
mode: subagent
-model: opencode/gemini-3-pro
+model: opencode/gemini-3.1-pro
---
You are a professional translator and localization specialist.
diff --git a/.opencode/tool/github-triage.ts b/.opencode/tool/github-triage.ts
index ed80f49d54..8ad0212ad0 100644
--- a/.opencode/tool/github-triage.ts
+++ b/.opencode/tool/github-triage.ts
@@ -5,8 +5,16 @@ import DESCRIPTION from "./github-triage.txt"
const TEAM = {
desktop: ["adamdotdevin", "iamdavidhill", "Brendonovich", "nexxeln"],
zen: ["fwang", "MrMushrooooom"],
- tui: ["thdxr", "kommander", "rekram1-node"],
- core: ["thdxr", "rekram1-node", "jlongster"],
+ tui: [
+ "thdxr",
+ "kommander",
+ // "rekram1-node" (on vacation)
+ ],
+ core: [
+ "thdxr",
+ // "rekram1-node", (on vacation)
+ "jlongster",
+ ],
docs: ["R44VC0RP"],
windows: ["Hona"],
} as const
@@ -42,10 +50,7 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) {
export default tool({
description: DESCRIPTION,
args: {
- assignee: tool.schema
- .enum(ASSIGNEES as [string, ...string[]])
- .describe("The username of the assignee")
- .default("rekram1-node"),
+ assignee: tool.schema.enum(ASSIGNEES as [string, ...string[]]).describe("The username of the assignee"),
labels: tool.schema
.array(tool.schema.enum(["nix", "opentui", "perf", "web", "desktop", "zen", "docs", "windows", "core"]))
.describe("The labels(s) to add to the issue")
@@ -68,7 +73,8 @@ export default tool({
results.push("Dropped label: nix (issue does not mention nix)")
}
- const assignee = nix ? "rekram1-node" : web ? pick(TEAM.desktop) : args.assignee
+ // const assignee = nix ? "rekram1-node" : web ? pick(TEAM.desktop) : args.assignee
+ const assignee = web ? pick(TEAM.desktop) : args.assignee
if (labels.includes("zen") && !zen) {
throw new Error("Only add the zen label when issue title/body contains 'zen'")
diff --git a/.opencode/tool/github-triage.txt b/.opencode/tool/github-triage.txt
index 4369ed2351..1a2d69bdb5 100644
--- a/.opencode/tool/github-triage.txt
+++ b/.opencode/tool/github-triage.txt
@@ -4,3 +4,5 @@ Choose labels and assignee using the current triage policy and ownership rules.
Pick the most fitting labels for the issue and assign one owner.
If unsure, choose the team/section with the most overlap with the issue and assign a member from that team at random.
+
+(Note: rekram1-node is on vacation, do not assign issues to him.)
diff --git a/README.ar.md b/README.ar.md
index f24e598d5e..aeb2f04b72 100644
--- a/README.ar.md
+++ b/README.ar.md
@@ -32,7 +32,8 @@
Português (Brasil) |
ไทย |
Türkçe |
- Українська
+ Українська |
+ বাংলা
[](https://opencode.ai)
diff --git a/README.bn.md b/README.bn.md
new file mode 100644
index 0000000000..a3ffdc0d80
--- /dev/null
+++ b/README.bn.md
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
+ওপেন সোর্স এআই কোডিং এজেন্ট।
+
+
+
+
+
+
+
+ English |
+ 简体中文 |
+ 繁體中文 |
+ 한국어 |
+ Deutsch |
+ Español |
+ Français |
+ Italiano |
+ Dansk |
+ 日本語 |
+ Polski |
+ Русский |
+ Bosanski |
+ العربية |
+ Norsk |
+ Português (Brasil) |
+ ไทย |
+ Türkçe |
+ Українська |
+ বাংলা
+
+
+[](https://opencode.ai)
+
+---
+
+### ইনস্টলেশন (Installation)
+
+```bash
+# YOLO
+curl -fsSL https://opencode.ai/install | bash
+
+# Package managers
+npm i -g opencode-ai@latest # or bun/pnpm/yarn
+scoop install opencode # Windows
+choco install opencode # Windows
+brew install anomalyco/tap/opencode # macOS and Linux (recommended, always up to date)
+brew install opencode # macOS and Linux (official brew formula, updated less)
+sudo pacman -S opencode # Arch Linux (Stable)
+paru -S opencode-bin # Arch Linux (Latest from AUR)
+mise use -g opencode # Any OS
+nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev branch
+```
+
+> [!TIP]
+> ইনস্টল করার আগে ০.১.x এর চেয়ে পুরোনো ভার্সনগুলো মুছে ফেলুন।
+
+### ডেস্কটপ অ্যাপ (BETA)
+
+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`, or AppImage |
+
+```bash
+# macOS (Homebrew)
+brew install --cask opencode-desktop
+# Windows (Scoop)
+scoop bucket add extras; scoop install extras/opencode-desktop
+```
+
+#### ইনস্টলেশন ডিরেক্টরি (Installation Directory)
+
+ইনস্টল স্ক্রিপ্টটি ইনস্টলেশন পাতের জন্য নিম্নলিখিত অগ্রাধিকার ক্রম মেনে চলে:
+
+1. `$OPENCODE_INSTALL_DIR` - কাস্টম ইনস্টলেশন ডিরেক্টরি
+2. `$XDG_BIN_DIR` - XDG বেস ডিরেক্টরি স্পেসিফিকেশন সমর্থিত পাথ
+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
+```
+
+### এজেন্টস (Agents)
+
+OpenCode এ দুটি বিল্ট-ইন এজেন্ট রয়েছে যা আপনি `Tab` কি(key) দিয়ে পরিবর্তন করতে পারবেন।
+
+- **build** - ডিফল্ট, ডেভেলপমেন্টের কাজের জন্য সম্পূর্ণ অ্যাক্সেসযুক্ত এজেন্ট
+- **plan** - বিশ্লেষণ এবং কোড এক্সপ্লোরেশনের জন্য রিড-ওনলি এজেন্ট
+ - ডিফল্টভাবে ফাইল এডিট করতে দেয় না
+ - ব্যাশ কমান্ড চালানোর আগে অনুমতি চায়
+ - অপরিচিত কোডবেস এক্সপ্লোর করা বা পরিবর্তনের পরিকল্পনা করার জন্য আদর্শ
+
+এছাড়াও জটিল অনুসন্ধান এবং মাল্টিস্টেপ টাস্কের জন্য একটি **general** সাবএজেন্ট অন্তর্ভুক্ত রয়েছে।
+এটি অভ্যন্তরীণভাবে ব্যবহৃত হয় এবং মেসেজে `@general` লিখে ব্যবহার করা যেতে পারে।
+
+এজেন্টদের সম্পর্কে আরও জানুন: [docs](https://opencode.ai/docs/agents)।
+
+### ডকুমেন্টেশন (Documentation)
+
+কিভাবে OpenCode কনফিগার করবেন সে সম্পর্কে আরও তথ্যের জন্য, [**আমাদের ডকস দেখুন**](https://opencode.ai/docs)।
+
+### অবদান (Contributing)
+
+আপনি যদি OpenCode এ অবদান রাখতে চান, অনুগ্রহ করে একটি পুল রিকোয়েস্ট সাবমিট করার আগে আমাদের [কন্ট্রিবিউটিং ডকস](./CONTRIBUTING.md) পড়ে নিন।
+
+### OpenCode এর উপর বিল্ডিং (Building on OpenCode)
+
+আপনি যদি এমন প্রজেক্টে কাজ করেন যা OpenCode এর সাথে সম্পর্কিত এবং প্রজেক্টের নামের অংশ হিসেবে "opencode" ব্যবহার করেন, উদাহরণস্বরূপ "opencode-dashboard" বা "opencode-mobile", তবে দয়া করে আপনার README তে একটি নোট যোগ করে স্পষ্ট করুন যে এই প্রজেক্টটি OpenCode দল দ্বারা তৈরি হয়নি এবং আমাদের সাথে এর কোনো সরাসরি সম্পর্ক নেই।
+
+### সচরাচর জিজ্ঞাসিত প্রশ্নাবলী (FAQ)
+
+#### এটি ক্লড কোড (Claude Code) থেকে কীভাবে আলাদা?
+
+ক্যাপাবিলিটির দিক থেকে এটি ক্লড কোডের (Claude Code) মতই। এখানে মূল পার্থক্যগুলো দেওয়া হলো:
+
+- ১০০% ওপেন সোর্স
+- কোনো প্রোভাইডারের সাথে আবদ্ধ নয়। যদিও আমরা [OpenCode Zen](https://opencode.ai/zen) এর মাধ্যমে মডেলসমূহ ব্যবহারের পরামর্শ দিই, OpenCode ক্লড (Claude), ওপেনএআই (OpenAI), গুগল (Google), অথবা লোকাল মডেলগুলোর সাথেও ব্যবহার করা যেতে পারে। যেমন যেমন মডেলগুলো উন্নত হবে, তাদের মধ্যকার পার্থক্য কমে আসবে এবং দামও কমবে, তাই প্রোভাইডার-অজ্ঞাস্টিক হওয়া খুবই গুরুত্বপূর্ণ।
+- আউট-অফ-দ্য-বক্স LSP সাপোর্ট
+- TUI এর উপর ফোকাস। OpenCode নিওভিম (neovim) ব্যবহারকারী এবং [terminal.shop](https://terminal.shop) এর নির্মাতাদের দ্বারা তৈরি; আমরা টার্মিনালে কী কী সম্ভব তার সীমাবদ্ধতা ছাড়িয়ে যাওয়ার চেষ্টা করছি।
+- ক্লায়েন্ট/সার্ভার আর্কিটেকচার। এটি যেমন OpenCode কে আপনার কম্পিউটারে চালানোর সুযোগ দেয়, তেমনি আপনি মোবাইল অ্যাপ থেকে রিমোটলি এটি নিয়ন্ত্রণ করতে পারবেন, অর্থাৎ TUI ফ্রন্টএন্ড কেবল সম্ভাব্য ক্লায়েন্টগুলোর মধ্যে একটি।
+
+---
+
+**আমাদের কমিউনিটিতে যুক্ত হোন** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
diff --git a/README.br.md b/README.br.md
index 4802c4996f..6044dad6c0 100644
--- a/README.br.md
+++ b/README.br.md
@@ -32,7 +32,8 @@
Português (Brasil) |
ไทย |
Türkçe |
- Українська
+ Українська |
+ বাংলা
[](https://opencode.ai)
diff --git a/README.bs.md b/README.bs.md
index 9ad6852018..ef54a83695 100644
--- a/README.bs.md
+++ b/README.bs.md
@@ -33,7 +33,8 @@
Português (Brasil) |
ไทย |
Türkçe |
- Українська
+ Українська |
+ বাংলা
[](https://opencode.ai)
diff --git a/README.da.md b/README.da.md
index 4b1302dbc3..3ffbbe8202 100644
--- a/README.da.md
+++ b/README.da.md
@@ -32,7 +32,8 @@
Português (Brasil) |
ไทย |
Türkçe |
- Українська
+ Українська |
+ বাংলা
[](https://opencode.ai)
diff --git a/README.de.md b/README.de.md
index 16116dc72f..64c6628b45 100644
--- a/README.de.md
+++ b/README.de.md
@@ -32,7 +32,8 @@
Português (Brasil) |
ไทย |
Türkçe |
- Українська
+ Українська |
+ বাংলা
[](https://opencode.ai)
diff --git a/README.es.md b/README.es.md
index 5c18ff4aca..875c8b0832 100644
--- a/README.es.md
+++ b/README.es.md
@@ -32,7 +32,8 @@
Português (Brasil) |
ไทย |
Türkçe |
- Українська
+ Українська |
+ বাংলা
[](https://opencode.ai)
diff --git a/README.fr.md b/README.fr.md
index 0382164bed..254d38577b 100644
--- a/README.fr.md
+++ b/README.fr.md
@@ -32,7 +32,8 @@
Português (Brasil) |
ไทย |
Türkçe |
- Українська
+ Українська |
+ বাংলা
[](https://opencode.ai)
diff --git a/README.it.md b/README.it.md
index c966ccec49..b1f75c2d2c 100644
--- a/README.it.md
+++ b/README.it.md
@@ -32,7 +32,8 @@
Português (Brasil) |
ไทย |
Türkçe |
- Українська
+ Українська |
+ বাংলা
[](https://opencode.ai)
diff --git a/README.ja.md b/README.ja.md
index 11109e7eb4..31d11dcf1a 100644
--- a/README.ja.md
+++ b/README.ja.md
@@ -32,7 +32,8 @@
Português (Brasil) |
ไทย |
Türkçe |
- Українська
+ Українська |
+ বাংলা
[](https://opencode.ai)
diff --git a/README.ko.md b/README.ko.md
index 23fea76b1e..5f9de2cf3f 100644
--- a/README.ko.md
+++ b/README.ko.md
@@ -32,7 +32,8 @@
Português (Brasil) |
ไทย |
Türkçe |
- Українська
+ Українська |
+ বাংলা
[](https://opencode.ai)
diff --git a/README.md b/README.md
index 99b4b2c50f..620415c961 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,8 @@
Português (Brasil) |
ไทย |
Türkçe |
- Українська
+ Українська |
+ বাংলা
[](https://opencode.ai)
diff --git a/README.no.md b/README.no.md
index 9b9e90dc38..125e181257 100644
--- a/README.no.md
+++ b/README.no.md
@@ -32,7 +32,8 @@
Português (Brasil) |
ไทย |
Türkçe |
- Українська
+ Українська |
+ বাংলা
[](https://opencode.ai)
diff --git a/README.pl.md b/README.pl.md
index fced98dfc3..61ef5870c1 100644
--- a/README.pl.md
+++ b/README.pl.md
@@ -32,7 +32,8 @@
Português (Brasil) |
ไทย |
Türkçe |
- Українська
+ Українська |
+ বাংলা
[](https://opencode.ai)
diff --git a/README.ru.md b/README.ru.md
index a7c590c16b..fed1101c0e 100644
--- a/README.ru.md
+++ b/README.ru.md
@@ -32,7 +32,8 @@
Português (Brasil) |
ไทย |
Türkçe |
- Українська
+ Українська |
+ বাংলা
[](https://opencode.ai)
diff --git a/README.th.md b/README.th.md
index 0999167f23..01d68dd8dc 100644
--- a/README.th.md
+++ b/README.th.md
@@ -32,7 +32,8 @@
Português (Brasil) |
ไทย |
Türkçe |
- Українська
+ Українська |
+ বাংলা
[](https://opencode.ai)
diff --git a/README.tr.md b/README.tr.md
index 67f84e4ddb..bfb18e1b43 100644
--- a/README.tr.md
+++ b/README.tr.md
@@ -32,7 +32,8 @@
Português (Brasil) |
ไทย |
Türkçe |
- Українська
+ Українська |
+ বাংলা
[](https://opencode.ai)
diff --git a/README.uk.md b/README.uk.md
index 77e859a45d..ed20fbf236 100644
--- a/README.uk.md
+++ b/README.uk.md
@@ -33,7 +33,8 @@
Português (Brasil) |
ไทย |
Türkçe |
- Українська
+ Українська |
+ বাংলা
[](https://opencode.ai)
diff --git a/README.zh.md b/README.zh.md
index 113d476b2e..c6d1c1d11f 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -32,7 +32,8 @@
Português (Brasil) |
ไทย |
Türkçe |
- Українська
+ Українська |
+ বাংলা
[](https://opencode.ai)
diff --git a/README.zht.md b/README.zht.md
index b518104443..0dd44a9f0f 100644
--- a/README.zht.md
+++ b/README.zht.md
@@ -32,7 +32,8 @@
Português (Brasil) |
ไทย |
Türkçe |
- Українська
+ Українська |
+ বাংলা
[](https://opencode.ai)
diff --git a/bun.lock b/bun.lock
index 2240f30558..04da112cf7 100644
--- a/bun.lock
+++ b/bun.lock
@@ -25,7 +25,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
- "version": "1.2.9",
+ "version": "1.2.10",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -75,7 +75,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
- "version": "1.2.9",
+ "version": "1.2.10",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -109,7 +109,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
- "version": "1.2.9",
+ "version": "1.2.10",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -136,7 +136,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
- "version": "1.2.9",
+ "version": "1.2.10",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -160,7 +160,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
- "version": "1.2.9",
+ "version": "1.2.10",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -184,7 +184,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
- "version": "1.2.9",
+ "version": "1.2.10",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -217,7 +217,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
- "version": "1.2.9",
+ "version": "1.2.10",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -246,7 +246,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
- "version": "1.2.9",
+ "version": "1.2.10",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -262,7 +262,7 @@
},
"packages/opencode": {
"name": "opencode",
- "version": "1.2.9",
+ "version": "1.2.10",
"bin": {
"opencode": "./bin/opencode",
},
@@ -376,7 +376,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
- "version": "1.2.9",
+ "version": "1.2.10",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -396,7 +396,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
- "version": "1.2.9",
+ "version": "1.2.10",
"devDependencies": {
"@hey-api/openapi-ts": "0.90.10",
"@tsconfig/node22": "catalog:",
@@ -407,7 +407,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
- "version": "1.2.9",
+ "version": "1.2.10",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -420,7 +420,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
- "version": "1.2.9",
+ "version": "1.2.10",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -462,7 +462,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
- "version": "1.2.9",
+ "version": "1.2.10",
"dependencies": {
"zod": "catalog:",
},
@@ -473,7 +473,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
- "version": "1.2.9",
+ "version": "1.2.10",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
diff --git a/github/action.yml b/github/action.yml
index 8652bb8c15..3d983a1609 100644
--- a/github/action.yml
+++ b/github/action.yml
@@ -30,6 +30,10 @@ inputs:
description: "Comma-separated list of trigger phrases (case-insensitive). Defaults to '/opencode,/oc'"
required: false
+ variant:
+ description: "Model variant for provider-specific reasoning effort (e.g., high, max, minimal)"
+ required: false
+
oidc_base_url:
description: "Base URL for OIDC token exchange API. Only required when running a custom GitHub App install. Defaults to https://api.opencode.ai"
required: false
@@ -71,4 +75,5 @@ runs:
PROMPT: ${{ inputs.prompt }}
USE_GITHUB_TOKEN: ${{ inputs.use_github_token }}
MENTIONS: ${{ inputs.mentions }}
+ VARIANT: ${{ inputs.variant }}
OIDC_BASE_URL: ${{ inputs.oidc_base_url }}
diff --git a/packages/app/e2e/actions.ts b/packages/app/e2e/actions.ts
index d42c0fcebb..a7ccba6175 100644
--- a/packages/app/e2e/actions.ts
+++ b/packages/app/e2e/actions.ts
@@ -225,7 +225,7 @@ export async function hoverSessionItem(page: Page, sessionID: string) {
export async function openSessionMoreMenu(page: Page, sessionID: string) {
await expect(page).toHaveURL(new RegExp(`/session/${sessionID}(?:[/?#]|$)`))
- const scroller = page.locator(".session-scroller").first()
+ const scroller = page.locator(".scroll-view__viewport").first()
await expect(scroller).toBeVisible()
await expect(scroller.getByRole("heading", { level: 1 }).first()).toBeVisible({ timeout: 30_000 })
diff --git a/packages/app/e2e/selectors.ts b/packages/app/e2e/selectors.ts
index be0bc05717..5fad2c06b5 100644
--- a/packages/app/e2e/selectors.ts
+++ b/packages/app/e2e/selectors.ts
@@ -20,11 +20,8 @@ export const settingsNotificationsAgentSelector = '[data-action="settings-notifi
export const settingsNotificationsPermissionsSelector = '[data-action="settings-notifications-permissions"]'
export const settingsNotificationsErrorsSelector = '[data-action="settings-notifications-errors"]'
export const settingsSoundsAgentSelector = '[data-action="settings-sounds-agent"]'
-export const settingsSoundsAgentEnabledSelector = '[data-action="settings-sounds-agent-enabled"]'
export const settingsSoundsPermissionsSelector = '[data-action="settings-sounds-permissions"]'
-export const settingsSoundsPermissionsEnabledSelector = '[data-action="settings-sounds-permissions-enabled"]'
export const settingsSoundsErrorsSelector = '[data-action="settings-sounds-errors"]'
-export const settingsSoundsErrorsEnabledSelector = '[data-action="settings-sounds-errors-enabled"]'
export const settingsUpdatesStartupSelector = '[data-action="settings-updates-startup"]'
export const settingsReleaseNotesSelector = '[data-action="settings-release-notes"]'
diff --git a/packages/app/e2e/session/session.spec.ts b/packages/app/e2e/session/session.spec.ts
index 93eaee5cb0..68d9929499 100644
--- a/packages/app/e2e/session/session.spec.ts
+++ b/packages/app/e2e/session/session.spec.ts
@@ -44,7 +44,7 @@ test("session can be renamed via header menu", async ({ page, sdk, gotoSession }
const menu = await openSessionMoreMenu(page, session.id)
await clickMenuItem(menu, /rename/i)
- const input = page.locator(".session-scroller").locator(inlineInputSelector).first()
+ const input = page.locator(".scroll-view__viewport").locator(inlineInputSelector).first()
await expect(input).toBeVisible()
await expect(input).toBeFocused()
await input.fill(renamedTitle)
diff --git a/packages/app/e2e/settings/settings.spec.ts b/packages/app/e2e/settings/settings.spec.ts
index 9fbcf79f5e..c2a8522eb0 100644
--- a/packages/app/e2e/settings/settings.spec.ts
+++ b/packages/app/e2e/settings/settings.spec.ts
@@ -9,7 +9,6 @@ import {
settingsNotificationsPermissionsSelector,
settingsReleaseNotesSelector,
settingsSoundsAgentSelector,
- settingsSoundsAgentEnabledSelector,
settingsSoundsErrorsSelector,
settingsSoundsPermissionsSelector,
settingsThemeSelector,
@@ -336,21 +335,19 @@ test("changing sound agent selection persists in localStorage", async ({ page, g
expect(stored?.sounds?.agent).not.toBe("staplebops-01")
})
-test("disabling agent sound disables sound selection", async ({ page, gotoSession }) => {
+test("selecting none disables agent sound", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = await openSettings(page)
const select = dialog.locator(settingsSoundsAgentSelector)
- const switchContainer = dialog.locator(settingsSoundsAgentEnabledSelector)
const trigger = select.locator('[data-slot="select-select-trigger"]')
await expect(select).toBeVisible()
- await expect(switchContainer).toBeVisible()
await expect(trigger).toBeEnabled()
- await switchContainer.locator('[data-slot="switch-control"]').click()
- await page.waitForTimeout(100)
-
- await expect(trigger).toBeDisabled()
+ await trigger.click()
+ const items = page.locator('[data-slot="select-select-item"]')
+ await expect(items.first()).toBeVisible()
+ await items.first().click()
const stored = await page.evaluate((key) => {
const raw = localStorage.getItem(key)
diff --git a/packages/app/e2e/terminal/terminal-init.spec.ts b/packages/app/e2e/terminal/terminal-init.spec.ts
index 87934b66e3..18991bf763 100644
--- a/packages/app/e2e/terminal/terminal-init.spec.ts
+++ b/packages/app/e2e/terminal/terminal-init.spec.ts
@@ -6,6 +6,7 @@ test("smoke terminal mounts and can create a second tab", async ({ page, gotoSes
await gotoSession()
const terminals = page.locator(terminalSelector)
+ const tabs = page.locator('#terminal-panel [data-slot="tabs-trigger"]')
const opened = await terminals.first().isVisible()
if (!opened) {
@@ -21,6 +22,7 @@ test("smoke terminal mounts and can create a second tab", async ({ page, gotoSes
await page.locator(promptSelector).click()
await page.keyboard.press("Control+Alt+T")
- await expect(terminals).toHaveCount(2)
- await expect(terminals.nth(1).locator("textarea")).toHaveCount(1)
+ await expect(tabs).toHaveCount(2)
+ await expect(terminals).toHaveCount(1)
+ await expect(terminals.first().locator("textarea")).toHaveCount(1)
})
diff --git a/packages/app/package.json b/packages/app/package.json
index 385205a0c1..b9397b0f40 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
- "version": "1.2.9",
+ "version": "1.2.10",
"description": "",
"type": "module",
"exports": {
diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx
index b1c608ffcc..adfd592f8d 100644
--- a/packages/app/src/components/prompt-input.tsx
+++ b/packages/app/src/components/prompt-input.tsx
@@ -89,6 +89,8 @@ const EXAMPLES = [
"prompt.example.25",
] as const
+const NON_EMPTY_TEXT = /[^\s\u200B]/
+
export const PromptInput: Component = (props) => {
const sdk = useSDK()
const sync = useSync()
@@ -636,7 +638,9 @@ export const PromptInput: Component = (props) => {
let buffer = ""
const flushText = () => {
- const content = buffer.replace(/\r\n?/g, "\n").replace(/\u200B/g, "")
+ let content = buffer
+ if (content.includes("\r")) content = content.replace(/\r\n?/g, "\n")
+ if (content.includes("\u200B")) content = content.replace(/\u200B/g, "")
buffer = ""
if (!content) return
parts.push({ type: "text", content, start: position, end: position + content.length })
@@ -714,10 +718,12 @@ export const PromptInput: Component = (props) => {
const rawParts = parseFromDOM()
const images = imageAttachments()
const cursorPosition = getCursorPosition(editorRef)
- const rawText = rawParts.map((p) => ("content" in p ? p.content : "")).join("")
- const trimmed = rawText.replace(/\u200B/g, "").trim()
+ const rawText =
+ rawParts.length === 1 && rawParts[0]?.type === "text"
+ ? rawParts[0].content
+ : rawParts.map((p) => ("content" in p ? p.content : "")).join("")
const hasNonText = rawParts.some((part) => part.type !== "text")
- const shouldReset = trimmed.length === 0 && !hasNonText && images.length === 0
+ const shouldReset = !NON_EMPTY_TEXT.test(rawText) && !hasNonText && images.length === 0
if (shouldReset) {
closePopover()
@@ -757,19 +763,31 @@ export const PromptInput: Component = (props) => {
}
const addPart = (part: ContentPart) => {
- const selection = window.getSelection()
- if (!selection || selection.rangeCount === 0) return
+ if (part.type === "image") return false
- const cursorPosition = getCursorPosition(editorRef)
- const currentPrompt = prompt.current()
- const rawText = currentPrompt.map((p) => ("content" in p ? p.content : "")).join("")
- const textBeforeCursor = rawText.substring(0, cursorPosition)
- const atMatch = textBeforeCursor.match(/@(\S*)$/)
+ const selection = window.getSelection()
+ if (!selection) return false
+
+ if (selection.rangeCount === 0 || !editorRef.contains(selection.anchorNode)) {
+ editorRef.focus()
+ const cursor = prompt.cursor() ?? promptLength(prompt.current())
+ setCursorPosition(editorRef, cursor)
+ }
+
+ if (selection.rangeCount === 0) return false
+ const range = selection.getRangeAt(0)
+ if (!editorRef.contains(range.startContainer)) return false
if (part.type === "file" || part.type === "agent") {
+ const cursorPosition = getCursorPosition(editorRef)
+ const rawText = prompt
+ .current()
+ .map((p) => ("content" in p ? p.content : ""))
+ .join("")
+ const textBeforeCursor = rawText.substring(0, cursorPosition)
+ const atMatch = textBeforeCursor.match(/@(\S*)$/)
const pill = createPill(part)
const gap = document.createTextNode(" ")
- const range = selection.getRangeAt(0)
if (atMatch) {
const start = atMatch.index ?? cursorPosition - atMatch[0].length
@@ -784,8 +802,9 @@ export const PromptInput: Component = (props) => {
range.collapse(true)
selection.removeAllRanges()
selection.addRange(range)
- } else if (part.type === "text") {
- const range = selection.getRangeAt(0)
+ }
+
+ if (part.type === "text") {
const fragment = createTextFragment(part.content)
const last = fragment.lastChild
range.deleteContents()
@@ -821,6 +840,7 @@ export const PromptInput: Component = (props) => {
handleInput()
closePopover()
+ return true
}
const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => {
diff --git a/packages/app/src/components/prompt-input/attachments.ts b/packages/app/src/components/prompt-input/attachments.ts
index 9ea2e62a65..a9e4e49651 100644
--- a/packages/app/src/components/prompt-input/attachments.ts
+++ b/packages/app/src/components/prompt-input/attachments.ts
@@ -7,6 +7,19 @@ import { getCursorPosition } from "./editor-dom"
export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
export const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"]
+const LARGE_PASTE_CHARS = 8000
+const LARGE_PASTE_BREAKS = 120
+
+function largePaste(text: string) {
+ if (text.length >= LARGE_PASTE_CHARS) return true
+ let breaks = 0
+ for (const char of text) {
+ if (char !== "\n") continue
+ breaks += 1
+ if (breaks >= LARGE_PASTE_BREAKS) return true
+ }
+ return false
+}
type PromptAttachmentsInput = {
editor: () => HTMLDivElement | undefined
@@ -14,7 +27,7 @@ type PromptAttachmentsInput = {
isDialogActive: () => boolean
setDraggingType: (type: "image" | "@mention" | null) => void
focusEditor: () => void
- addPart: (part: ContentPart) => void
+ addPart: (part: ContentPart) => boolean
readClipboardImage?: () => Promise
}
@@ -89,6 +102,13 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
}
if (!plainText) return
+
+ if (largePaste(plainText)) {
+ if (input.addPart({ type: "text", content: plainText, start: 0, end: 0 })) return
+ input.focusEditor()
+ if (input.addPart({ type: "text", content: plainText, start: 0, end: 0 })) return
+ }
+
const inserted = typeof document.execCommand === "function" && document.execCommand("insertText", false, plainText)
if (inserted) return
diff --git a/packages/app/src/components/prompt-input/editor-dom.test.ts b/packages/app/src/components/prompt-input/editor-dom.test.ts
index 15e759f44a..3088522a59 100644
--- a/packages/app/src/components/prompt-input/editor-dom.test.ts
+++ b/packages/app/src/components/prompt-input/editor-dom.test.ts
@@ -24,6 +24,28 @@ describe("prompt-input editor dom", () => {
expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR")
})
+ test("createTextFragment avoids break-node explosion for large multiline content", () => {
+ const content = Array.from({ length: 220 }, () => "line").join("\n")
+ const fragment = createTextFragment(content)
+ const container = document.createElement("div")
+ container.appendChild(fragment)
+
+ expect(container.childNodes.length).toBe(1)
+ expect(container.childNodes[0]?.nodeType).toBe(Node.TEXT_NODE)
+ expect(container.textContent).toBe(content)
+ })
+
+ test("createTextFragment keeps terminal break in large multiline fallback", () => {
+ const content = `${Array.from({ length: 220 }, () => "line").join("\n")}\n`
+ const fragment = createTextFragment(content)
+ const container = document.createElement("div")
+ container.appendChild(fragment)
+
+ expect(container.childNodes.length).toBe(2)
+ expect(container.childNodes[0]?.textContent).toBe(content.slice(0, -1))
+ expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR")
+ })
+
test("length helpers treat breaks as one char and ignore zero-width chars", () => {
const container = document.createElement("div")
container.appendChild(document.createTextNode("ab\u200B"))
diff --git a/packages/app/src/components/prompt-input/editor-dom.ts b/packages/app/src/components/prompt-input/editor-dom.ts
index 4850a26ece..8575140d7d 100644
--- a/packages/app/src/components/prompt-input/editor-dom.ts
+++ b/packages/app/src/components/prompt-input/editor-dom.ts
@@ -1,5 +1,20 @@
+const MAX_BREAKS = 200
+
export function createTextFragment(content: string): DocumentFragment {
const fragment = document.createDocumentFragment()
+ let breaks = 0
+ for (const char of content) {
+ if (char !== "\n") continue
+ breaks += 1
+ if (breaks > MAX_BREAKS) {
+ const tail = content.endsWith("\n")
+ const text = tail ? content.slice(0, -1) : content
+ if (text) fragment.appendChild(document.createTextNode(text))
+ if (tail) fragment.appendChild(document.createElement("br"))
+ return fragment
+ }
+ }
+
const segments = content.split("\n")
segments.forEach((segment, index) => {
if (segment) {
diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx
index 162e016c6f..1ea97c395c 100644
--- a/packages/app/src/components/session/session-context-tab.tsx
+++ b/packages/app/src/components/session/session-context-tab.tsx
@@ -11,6 +11,7 @@ import { Accordion } from "@opencode-ai/ui/accordion"
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
import { Code } from "@opencode-ai/ui/code"
import { Markdown } from "@opencode-ai/ui/markdown"
+import { ScrollView } from "@opencode-ai/ui/scroll-view"
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
import { useLanguage } from "@/context/language"
import { getSessionContextMetrics } from "./session-context-metrics"
@@ -268,9 +269,9 @@ export function SessionContextTab() {
})
return (
- {
+ {
scroll = el
restoreScroll()
}}
@@ -336,6 +337,6 @@ export function SessionContextTab() {
-
+
)
}
diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx
index ae8fc200f2..825d1dab6c 100644
--- a/packages/app/src/components/session/session-header.tsx
+++ b/packages/app/src/components/session/session-header.tsx
@@ -452,7 +452,10 @@ export function SessionHeader() {
variant: "ghost",
class:
"rounded-md h-[24px] px-3 border border-border-weak-base bg-surface-panel shadow-none data-[expanded]:bg-surface-base-active",
- classList: { "rounded-r-none": share.shareUrl() !== undefined },
+ classList: {
+ "rounded-r-none": share.shareUrl() !== undefined,
+ "border-r-0": share.shareUrl() !== undefined,
+ },
style: { scale: 1 },
}}
trigger={{language.t("session.share.action.share")}}
diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx
index df71fd77e8..cf993840dc 100644
--- a/packages/app/src/components/settings-general.tsx
+++ b/packages/app/src/components/settings-general.tsx
@@ -20,12 +20,17 @@ let demoSoundState = {
// To prevent audio from overlapping/playing very quickly when navigating the settings menus,
// delay the playback by 100ms during quick selection changes and pause existing sounds.
-const playDemoSound = (src: string) => {
+const stopDemoSound = () => {
if (demoSoundState.cleanup) {
demoSoundState.cleanup()
}
-
clearTimeout(demoSoundState.timeout)
+ demoSoundState.cleanup = undefined
+}
+
+const playDemoSound = (src: string | undefined) => {
+ stopDemoSound()
+ if (!src) return
demoSoundState.timeout = setTimeout(() => {
demoSoundState.cleanup = playSound(src)
@@ -132,11 +137,17 @@ export const SettingsGeneral: Component = () => {
] as const
const fontOptionsList = [...fontOptions]
- const soundOptions = [...SOUND_OPTIONS]
+ const noneSound = { id: "none", label: "sound.option.none", src: undefined } as const
+ const soundOptions = [noneSound, ...SOUND_OPTIONS]
- const soundSelectProps = (current: () => string, set: (id: string) => void) => ({
+ const soundSelectProps = (
+ enabled: () => boolean,
+ current: () => string,
+ setEnabled: (value: boolean) => void,
+ set: (id: string) => void,
+ ) => ({
options: soundOptions,
- current: soundOptions.find((o) => o.id === current()),
+ current: enabled() ? (soundOptions.find((o) => o.id === current()) ?? noneSound) : noneSound,
value: (o: (typeof soundOptions)[number]) => o.id,
label: (o: (typeof soundOptions)[number]) => language.t(o.label),
onHighlight: (option: (typeof soundOptions)[number] | undefined) => {
@@ -145,6 +156,12 @@ export const SettingsGeneral: Component = () => {
},
onSelect: (option: (typeof soundOptions)[number] | undefined) => {
if (!option) return
+ if (option.id === "none") {
+ setEnabled(false)
+ stopDemoSound()
+ return
+ }
+ setEnabled(true)
set(option.id)
playDemoSound(option.src)
},
@@ -250,6 +267,18 @@ export const SettingsGeneral: Component = () => {
)}
+
+
+
+ settings.general.setShowReasoningSummaries(checked)}
+ />
+
+
)
@@ -307,66 +336,45 @@ export const SettingsGeneral: Component = () => {
title={language.t("settings.general.sounds.agent.title")}
description={language.t("settings.general.sounds.agent.description")}
>
-
-
- settings.sounds.setAgentEnabled(checked)}
- />
-
-
+