Compare commits

..

56 Commits

Author SHA1 Message Date
opencode f9cef22a53 release: v1.0.181 2025-12-21 02:06:25 +00:00
Adam b5d7d3dec1 fix(desktop): layout 2025-12-20 20:02:40 -06:00
Adam 182630e0d7 feat(desktop): new layout 2025-12-20 19:52:12 -06:00
YuY801103 c81506b28d docs: add Traditional Chinese (Taiwan) README translation (#5861)
Co-authored-by: Yu <YuY801103@users.noreply.github.com>
2025-12-20 15:51:42 -06:00
Ryan Vogel 6c40bfe043 docs: clarify model ID format for OpenCode provider (#5854) 2025-12-20 13:51:13 -06:00
Aiden Cline 9caaae6a18 tweak: better error message if no primary agents are enabled 2025-12-20 13:47:28 -06:00
Ryan Vogel ad6a5e6157 feat(docs): adding .md to docs pages shows raw markdown (#5823) 2025-12-20 12:05:06 -06:00
shamil2 7dd8ea58c2 feat: add Catppuccin Frappé theme (#5821)
Co-authored-by: shamil2 <shamil2@users.noreply.github.com>
2025-12-20 12:04:35 -06:00
ja 3b261e0125 docs: add name property to model configuration example (#5853) 2025-12-20 11:54:49 -06:00
Shpetim 426791f68a fix: system theme flicker (#5842)
Co-authored-by: Shpetim <shpetim.alimi@ndbit.net>
2025-12-20 11:53:46 -06:00
Frank c7cade2494 zen: sync 2025-12-20 12:52:55 -05:00
Matt Silverlock 8f6c8844d7 feat: support configuring a default_agent across all API/user surfaces (#5843)
Co-authored-by: observerw <observerw@users.noreply.github.com>
2025-12-20 11:49:23 -06:00
Aiden Cline da6e0e60c0 ci: adjust review agent prompt to discourage bad diffs 2025-12-20 11:43:59 -06:00
lif d89b567b47 fix: add transform case for gemini if mcp tool has missing array items (#5846) 2025-12-20 11:41:52 -06:00
ja 34eb03f5b8 fix: prioritize session list loading when resuming with -c (#5816)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-20 11:39:26 -06:00
Ryan Cassidy 2f6d15a51e feat: add cursor theme (#5850) 2025-12-20 10:56:27 -06:00
GitHub Action 8ffea80980 chore: generate 2025-12-20 16:00:36 +00:00
opencode c87d61b561 release: v1.0.180 2025-12-20 16:00:36 +00:00
Dax Raad 35c12e2053 ci 2025-12-20 10:54:02 -05:00
opencode 33d8bfc937 release: v1.0.179 2025-12-20 15:38:38 +00:00
Dax Raad f2343a6794 ci 2025-12-20 10:35:01 -05:00
GitHub Action bab000eeb5 chore: generate 2025-12-20 15:29:23 +00:00
Dax Raad 8e674ae053 ci 2025-12-20 10:28:46 -05:00
opencode 6a4f4009d5 release: v1.0.178 2025-12-20 15:08:27 +00:00
Dax Raad 5e79b95927 ci 2025-12-20 10:05:03 -05:00
Tommy D. Rossi a7a2bbb497 feat: add endpoints to delete and update message parts (#5433) 2025-12-20 15:00:41 +00:00
GitHub Action 6e93d14bdb chore: generate 2025-12-20 15:00:41 +00:00
opencode f29f284b3e release: v1.0.177 2025-12-20 15:00:40 +00:00
Dax Raad b1b8f6cf71 ci 2025-12-20 09:57:19 -05:00
opencode 4c3336bbe7 release: v1.0.176 2025-12-20 12:28:27 +00:00
Adam 354ac0b493 fix(desktop): sidebar UX issues 2025-12-20 06:25:39 -06:00
Adam 1d159c6858 fix(desktop): task rendering perf 2025-12-20 06:15:59 -06:00
GitHub Action d70639b256 chore: generate 2025-12-20 12:13:52 +00:00
Adam e4a92f0084 fix(desktop): show last text part when summarized 2025-12-20 06:12:58 -06:00
Adam fdf5a70a27 fix(desktop): performance with lots of session changes 2025-12-20 06:12:58 -06:00
Adam f71da42520 fix(desktop): event reconnect gaps 2025-12-20 06:12:58 -06:00
GitHub Action f6bdeb9e3a ignore: update download stats 2025-12-20 2025-12-20 12:04:14 +00:00
opencode 2400354bab release: v1.0.175 2025-12-20 11:10:23 +00:00
Adam db348c46cc fix(desktop): perf tweaks 2025-12-20 05:06:57 -06:00
Adam 49567fe61a fix(desktop): add retries to init promises 2025-12-20 04:57:39 -06:00
Adam e5b3f796e4 fix: types 2025-12-20 04:33:42 -06:00
Adam a9700c8773 fix: shouldEncode 2025-12-20 04:25:17 -06:00
Adam 26cf5e003e fix(desktop): perf stuff 2025-12-20 04:25:17 -06:00
Adam 742cf10dee fix(desktop): removed projects 2025-12-20 04:25:16 -06:00
Frank 7664453f94 zen: add minimax m2.1 2025-12-20 01:55:10 -05:00
GitHub Action 460672aa93 chore: generate 2025-12-20 06:27:02 +00:00
Frank b4e4fd9807 zen: add minimax m2.1 2025-12-20 01:26:27 -05:00
opencode 34bdfd0937 release: v1.0.174 2025-12-20 04:06:28 +00:00
GitHub Action 84591ca8ad chore: generate 2025-12-20 03:58:36 +00:00
Aiden Cline fd4d0c5c0b fix: file permissions 2025-12-19 21:57:55 -06:00
opencode 9f5db46911 release: v1.0.173 2025-12-20 03:48:15 +00:00
Ariane Emory 755ddbb223 feat(tui): reinsert forked message text in prompt text input box when forking session (resolves #5495) (#5545)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-19 21:45:33 -06:00
GitHub Action 701d470d01 chore: generate 2025-12-20 03:42:00 +00:00
Aiden Cline 1d9058d26b ci: fix archive 2025-12-19 21:41:18 -06:00
opencode 39e2a5f595 release: v1.0.172 2025-12-20 03:29:07 +00:00
Aiden Cline f862ab6722 ci: fix undefined 2025-12-19 21:22:49 -06:00
79 changed files with 1759 additions and 477 deletions
+11 -14
View File
@@ -80,8 +80,9 @@ jobs:
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
NPM_CONFIG_PROVENANCE: false
outputs:
releaseId: ${{ steps.publish.outputs.releaseId }}
tagName: ${{ steps.publish.outputs.tagName }}
release: ${{ steps.publish.outputs.release }}
tag: ${{ steps.publish.outputs.tag }}
version: ${{ steps.publish.outputs.version }}
publish-tauri:
needs: publish
@@ -103,7 +104,7 @@ jobs:
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ needs.publish.outputs.tagName }}
ref: ${{ needs.publish.outputs.tag }}
- uses: apple-actions/import-codesign-certs@v2
if: ${{ runner.os == 'macOS' }}
@@ -146,20 +147,18 @@ jobs:
shared-key: ${{ matrix.settings.target }}
- name: Prepare
if: inputs.bump || inputs.version
run: |
cd packages/tauri
bun ./scripts/prepare.ts
env:
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_VERSION: ${{ inputs.version }}
OPENCODE_CHANNEL: latest
OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
RUST_TARGET: ${{ matrix.settings.target }}
GH_TOKEN: ${{ github.token }}
OPENCODE_RELEASE_TAG: ${{ needs.publish.outputs.tagName }}
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
- name: Install tauri-cli from portable appimage branch
@@ -189,8 +188,8 @@ jobs:
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
args: --target ${{ matrix.settings.target }} --config src-tauri/tauri.prod.conf.json
updaterJsonPreferNsis: true
releaseId: ${{ needs.publish.outputs.releaseId }}
tagName: ${{ needs.publish.outputs.tagName }}
releaseId: ${{ needs.publish.outputs.release }}
tagName: ${{ needs.publish.outputs.tag }}
releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
releaseDraft: true
@@ -198,13 +197,13 @@ jobs:
needs:
- publish
- publish-tauri
if: needs.publish.outputs.tagName
if: needs.publish.outputs.tag
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ needs.publish.outputs.tagName }}
ref: ${{ needs.publish.outputs.tag }}
- uses: ./.github/actions/setup-bun
@@ -221,8 +220,6 @@ jobs:
- run: ./script/publish-complete.ts
env:
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_VERSION: ${{ inputs.version }}
OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
AUR_KEY: ${{ secrets.AUR_KEY }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
OPENCODE_RELEASE_TAG: ${{ needs.publish.outputs.tagName }}
+2
View File
@@ -67,6 +67,8 @@ jobs:
When critiquing code style don't be a zealot, we don't like "let" statements but sometimes they are the simpliest option, if someone does a bunch of nesting with let, they should consider using iife (see packages/opencode/src/util.iife.ts)
Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block.
If you are writing suggested fixes, BE SURE THAT the change you are recommending is actually valid typescript, often I have seen missing closing "}" or other syntax errors.
Generally, write a comment instead of writing suggested change if you can help it.
Command MUST be like this.
\`\`\`
+115
View File
@@ -0,0 +1,115 @@
<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 Coding Agent。</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/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></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 bucket add extras; scoop install extras/opencode # Windows
choco install opencode # Windows
brew install opencode # macOS 與 Linux
paru -S opencode-bin # Arch Linux
mise use -g github:sst/opencode # 任何作業系統
nix run nixpkgs#opencode # 或使用 github:sst/opencode 以取得最新開發分支
```
> [!TIP]
> 安裝前請先移除 0.1.x 以前的舊版本。
### 桌面應用程式 (BETA)
OpenCode 也提供桌面版應用程式。您可以直接從 [發佈頁面 (releases page)](https://github.com/sst/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 Cask)
brew install --cask opencode-desktop
```
#### 安裝目錄
安裝腳本會依據以下優先順序決定安裝路徑:
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 內建了兩種 Agent,您可以使用 `Tab` 鍵快速切換。
- **build** - 預設模式,具備完整權限的 Agent,適用於開發工作。
- **plan** - 唯讀模式,適用於程式碼分析與探索。
- 預設禁止修改檔案。
- 執行 bash 指令前會詢問權限。
- 非常適合用來探索陌生的程式碼庫或規劃變更。
此外,OpenCode 還包含一個 **general** 子 Agent,用於處理複雜搜尋與多步驟任務。此 Agent 供系統內部使用,亦可透過在訊息中輸入 `@general` 來呼叫。
了解更多關於 [Agents](https://opencode.ai/docs/agents) 的資訊。
### 線上文件
關於如何設定 OpenCode 的詳細資訊,請參閱我們的 [**官方文件**](https://opencode.ai/docs)。
### 參與貢獻
如果您有興趣參與 OpenCode 的開發,請在提交 Pull Request 前先閱讀我們的 [貢獻指南 (Contributing Docs)](./CONTRIBUTING.md)。
### 基於 OpenCode 進行開發
如果您正在開發與 OpenCode 相關的專案,並在名稱中使用了 "opencode"(例如 "opencode-dashboard" 或 "opencode-mobile"),請在您的 README 中加入聲明,說明該專案並非由 OpenCode 團隊開發,且與我們沒有任何隸屬關係。
### 常見問題 (FAQ)
#### 這跟 Claude Code 有什麼不同?
在功能面上與 Claude Code 非常相似。以下是關鍵差異:
- 100% 開源。
- 不綁定特定的服務提供商。雖然我們推薦使用透過 [OpenCode Zen](https://opencode.ai/zen) 提供的模型,但 OpenCode 也可搭配 Claude, OpenAI, Google 甚至本地模型使用。隨著模型不斷演進,彼此間的差距會縮小且價格會下降,因此具備「不限廠商 (provider-agnostic)」的特性至關重要。
- 內建 LSP (語言伺服器協定) 支援。
- 專注於終端機介面 (TUI)。OpenCode 由 Neovim 愛好者與 [terminal.shop](https://terminal.shop) 的創作者打造;我們將不斷挑戰終端機介面的極限。
- 客戶端/伺服器架構 (Client/Server Architecture)。這讓 OpenCode 能夠在您的電腦上運行的同時,由行動裝置進行遠端操控。這意味著 TUI 前端只是眾多可能的客戶端之一。
#### 另一個同名的 Repo 是什麼?
另一個名稱相近的儲存庫與本專案無關。您可以點此[閱讀背後的故事](https://x.com/thdxr/status/1933561254481666466)。
---
**加入我們的社群** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
+1
View File
@@ -175,3 +175,4 @@
| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) |
| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) |
| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) |
| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) |
+15 -15
View File
@@ -20,7 +20,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.0.170",
"version": "1.0.181",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -48,7 +48,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.0.170",
"version": "1.0.181",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -75,7 +75,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.0.170",
"version": "1.0.181",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -99,7 +99,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.0.170",
"version": "1.0.181",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -123,7 +123,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.0.170",
"version": "1.0.181",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -171,7 +171,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.0.170",
"version": "1.0.181",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -200,7 +200,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.0.170",
"version": "1.0.181",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -216,7 +216,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.0.170",
"version": "1.0.181",
"bin": {
"opencode": "./bin/opencode",
},
@@ -308,7 +308,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.0.170",
"version": "1.0.181",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -328,7 +328,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.0.170",
"version": "1.0.181",
"devDependencies": {
"@hey-api/openapi-ts": "0.88.1",
"@tsconfig/node22": "catalog:",
@@ -339,7 +339,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.0.170",
"version": "1.0.181",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -352,7 +352,7 @@
},
"packages/tauri": {
"name": "@opencode-ai/tauri",
"version": "1.0.170",
"version": "1.0.181",
"dependencies": {
"@opencode-ai/desktop": "workspace:*",
"@solid-primitives/storage": "catalog:",
@@ -379,7 +379,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.0.170",
"version": "1.0.181",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -414,7 +414,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.0.170",
"version": "1.0.181",
"dependencies": {
"zod": "catalog:",
},
@@ -425,7 +425,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.0.170",
"version": "1.0.181",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
+5
View File
@@ -9,6 +9,10 @@ inputs:
description: "Model to use"
required: true
agent:
description: "Agent to use. Must be a primary agent. Falls back to default_agent from config or 'build' if not found."
required: false
share:
description: "Share the opencode session (defaults to true for public repos)"
required: false
@@ -62,6 +66,7 @@ runs:
run: opencode github run
env:
MODEL: ${{ inputs.model }}
AGENT: ${{ inputs.agent }}
SHARE: ${{ inputs.share }}
PROMPT: ${{ inputs.prompt }}
USE_GITHUB_TOKEN: ${{ inputs.use_github_token }}
+27 -1
View File
@@ -318,6 +318,10 @@ function useEnvRunUrl() {
return `/${repo.owner}/${repo.repo}/actions/runs/${runId}`
}
function useEnvAgent() {
return process.env["AGENT"] || undefined
}
function useEnvShare() {
const value = process.env["SHARE"]
if (!value) return undefined
@@ -578,16 +582,38 @@ async function summarize(response: string) {
}
}
async function resolveAgent(): Promise<string | undefined> {
const envAgent = useEnvAgent()
if (!envAgent) return undefined
// Validate the agent exists and is a primary agent
const agents = await client.agent.list<true>()
const agent = agents.data?.find((a) => a.name === envAgent)
if (!agent) {
console.warn(`agent "${envAgent}" not found. Falling back to default agent`)
return undefined
}
if (agent.mode === "subagent") {
console.warn(`agent "${envAgent}" is a subagent, not a primary agent. Falling back to default agent`)
return undefined
}
return envAgent
}
async function chat(text: string, files: PromptFiles = []) {
console.log("Sending message to opencode...")
const { providerID, modelID } = useEnvModel()
const agent = await resolveAgent()
const chat = await client.session.chat<true>({
path: session,
body: {
providerID,
modelID,
agent: "build",
agent,
parts: [
{
type: "text",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.0.170",
"version": "1.0.181",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
@@ -202,6 +202,14 @@ export function IconZai(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconMiniMax(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z" />
</svg>
)
}
export function IconGemini(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} viewBox="0 0 50 50" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
@@ -9,6 +9,7 @@ import {
IconAlibaba,
IconAnthropic,
IconGemini,
IconMiniMax,
IconMoonshotAI,
IconOpenAI,
IconStealth,
@@ -23,6 +24,7 @@ const getModelLab = (modelId: string) => {
if (modelId.startsWith("kimi")) return "Moonshot AI"
if (modelId.startsWith("glm")) return "Z.ai"
if (modelId.startsWith("qwen")) return "Alibaba"
if (modelId.startsWith("minimax")) return "MiniMax"
if (modelId.startsWith("grok")) return "xAI"
return "Stealth"
}
@@ -35,7 +37,7 @@ const getModelsInfo = query(async (workspaceID: string) => {
.filter(([id, _model]) => !["claude-3-5-haiku"].includes(id))
.filter(([id, _model]) => !id.startsWith("alpha-"))
.sort(([idA, modelA], [idB, modelB]) => {
const priority = ["big-pickle", "grok", "claude", "gpt", "gemini"]
const priority = ["big-pickle", "minimax", "grok", "claude", "gpt", "gemini"]
const getPriority = (id: string) => {
const index = priority.findIndex((p) => id.startsWith(p))
return index === -1 ? Infinity : index
@@ -129,6 +131,8 @@ export function ModelSection() {
return <IconAlibaba width={16} height={16} />
case "xAI":
return <IconXai width={16} height={16} />
case "MiniMax":
return <IconMiniMax width={16} height={16} />
default:
return <IconStealth width={16} height={16} />
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.0.170",
"version": "1.0.181",
"private": true,
"type": "module",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.0.170",
"version": "1.0.181",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.0.170",
"version": "1.0.181",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/desktop",
"version": "1.0.170",
"version": "1.0.181",
"description": "",
"type": "module",
"exports": {
+1 -2
View File
@@ -8,8 +8,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
init: (props: { url: string }) => {
const eventSdk = createOpencodeClient({
baseUrl: props.url,
signal: AbortSignal.timeout(1000 * 60 * 10),
throwOnError: true,
// signal: AbortSignal.timeout(1000 * 60 * 10),
})
const emitter = createGlobalEmitter<{
[key: string]: Event
+25 -16
View File
@@ -18,6 +18,7 @@ import {
} from "@opencode-ai/sdk/v2/client"
import { createStore, produce, reconcile } from "solid-js/store"
import { Binary } from "@opencode-ai/util/binary"
import { retry } from "@opencode-ai/util/retry"
import { useGlobalSDK } from "./global-sdk"
import { ErrorPage, type InitError } from "../pages/error"
import { createContext, useContext, onMount, type ParentProps, Switch, Match } from "solid-js"
@@ -145,7 +146,7 @@ function createGlobalSync() {
changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)),
node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
}
await Promise.all(Object.values(load).map((p) => p().catch((e) => setGlobalStore("error", e))))
await Promise.all(Object.values(load).map((p) => retry(p).catch((e) => setGlobalStore("error", e))))
.then(() => setStore("ready", true))
.catch((e) => setGlobalStore("error", e))
}
@@ -295,21 +296,29 @@ function createGlobalSync() {
async function bootstrap() {
return Promise.all([
globalSDK.client.path.get().then((x) => {
setGlobalStore("path", x.data!)
}),
globalSDK.client.project.list().then(async (x) => {
setGlobalStore(
"project",
x.data!.filter((p) => !p.worktree.includes("opencode-test")).sort((a, b) => a.id.localeCompare(b.id)),
)
}),
globalSDK.client.provider.list().then((x) => {
setGlobalStore("provider", x.data ?? {})
}),
globalSDK.client.provider.auth().then((x) => {
setGlobalStore("provider_auth", x.data ?? {})
}),
retry(() =>
globalSDK.client.path.get().then((x) => {
setGlobalStore("path", x.data!)
}),
),
retry(() =>
globalSDK.client.project.list().then(async (x) => {
setGlobalStore(
"project",
x.data!.filter((p) => !p.worktree.includes("opencode-test")).sort((a, b) => a.id.localeCompare(b.id)),
)
}),
),
retry(() =>
globalSDK.client.provider.list().then((x) => {
setGlobalStore("provider", x.data ?? {})
}),
),
retry(() =>
globalSDK.client.provider.auth().then((x) => {
setGlobalStore("provider_auth", x.data ?? {})
}),
),
])
.then(() => setGlobalStore("ready", true))
.catch((e) => setGlobalStore("error", e))
+20 -22
View File
@@ -27,6 +27,8 @@ type SessionTabs = {
all: string[]
}
export type LocalProject = Partial<Project> & { worktree: string; expanded: boolean }
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
name: "Layout",
init: () => {
@@ -44,8 +46,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
opened: false,
height: 280,
},
review: {
state: "pane" as "pane" | "tab",
session: {
width: 600,
},
sessionTabs: {} as Record<string, SessionTabs>,
}),
@@ -61,21 +63,22 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
function enrich(project: { worktree: string; expanded: boolean }) {
const metadata = globalSync.data.project.find((x) => x.worktree === project.worktree)
if (!metadata) return []
return [
{
...project,
...metadata,
...(metadata ?? {}),
},
]
}
function colorize(project: Project & { expanded: boolean }) {
function colorize(project: LocalProject) {
if (project.icon?.color) return project
const color = pickAvailableColor()
usedColors.add(color)
project.icon = { ...project.icon, color }
globalSdk.client.project.update({ projectID: project.id, icon: { color } })
if (project.id) {
globalSdk.client.project.update({ projectID: project.id, icon: { color } })
}
return project
}
@@ -95,7 +98,9 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
projects: {
list,
open(directory: string) {
if (store.projects.find((x) => x.worktree === directory)) return
if (store.projects.find((x) => x.worktree === directory)) {
return
}
globalSync.project.loadSessions(directory)
setStore("projects", (x) => [{ worktree: directory, expanded: true }, ...x])
},
@@ -151,13 +156,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setStore("terminal", "height", height)
},
},
review: {
state: createMemo(() => store.review?.state ?? "closed"),
pane() {
setStore("review", "state", "pane")
},
tab() {
setStore("review", "state", "tab")
session: {
width: createMemo(() => store.session?.width ?? 600),
resize(width: number) {
if (!store.session) {
setStore("session", { width })
} else {
setStore("session", "width", width)
}
},
},
tabs(sessionKey: string) {
@@ -181,14 +187,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}
},
async open(tab: string) {
if (tab === "chat") {
if (!store.sessionTabs[sessionKey]) {
setStore("sessionTabs", sessionKey, { all: [], active: undefined })
} else {
setStore("sessionTabs", sessionKey, "active", undefined)
}
return
}
const current = store.sessionTabs[sessionKey] ?? { all: [] }
if (tab !== "review") {
if (!current.all.includes(tab)) {
+2 -1
View File
@@ -337,6 +337,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const load = async (path: string) => {
const relativePath = relative(path)
await sdk.client.file.read({ path: relativePath }).then((x) => {
if (!store.node[relativePath]) return
setStore(
"node",
relativePath,
@@ -425,7 +426,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
init,
expand(path: string) {
setStore("node", path, "expanded", true)
if (store.node[path].loaded) return
if (store.node[path]?.loaded) return
setStore("node", path, "loaded", true)
list(path)
},
+5 -4
View File
@@ -1,6 +1,7 @@
import { produce } from "solid-js/store"
import { createMemo } from "solid-js"
import { Binary } from "@opencode-ai/util/binary"
import { retry } from "@opencode-ai/util/retry"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSync } from "./global-sync"
import { useSDK } from "./sdk"
@@ -61,10 +62,10 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
},
async sync(sessionID: string, _isRetry = false) {
const [session, messages, todo, diff] = await Promise.all([
sdk.client.session.get({ sessionID }, { throwOnError: true }),
sdk.client.session.messages({ sessionID, limit: 100 }),
sdk.client.session.todo({ sessionID }),
sdk.client.session.diff({ sessionID }),
retry(() => sdk.client.session.get({ sessionID })),
retry(() => sdk.client.session.messages({ sessionID, limit: 100 })),
retry(() => sdk.client.session.todo({ sessionID })),
retry(() => sdk.client.session.diff({ sessionID })),
])
setStore(
produce((draft) => {
+18 -21
View File
@@ -1,18 +1,7 @@
import {
createEffect,
createMemo,
createSignal,
For,
Match,
onMount,
ParentProps,
Show,
Switch,
type JSX,
} from "solid-js"
import { createEffect, createMemo, For, Match, onMount, ParentProps, Show, Switch, type JSX } from "solid-js"
import { DateTime } from "luxon"
import { A, useNavigate, useParams } from "@solidjs/router"
import { useLayout, getAvatarColors } from "@/context/layout"
import { useLayout, getAvatarColors, LocalProject } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync"
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
import { Avatar } from "@opencode-ai/ui/avatar"
@@ -26,7 +15,7 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { Spinner } from "@opencode-ai/ui/spinner"
import { getFilename } from "@opencode-ai/util/path"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Session, Project } from "@opencode-ai/sdk/v2/client"
import { Session } from "@opencode-ai/sdk/v2/client"
import { usePlatform } from "@/context/platform"
import { createStore, produce } from "solid-js/store"
import {
@@ -348,7 +337,7 @@ export default function Layout(props: ParentProps) {
}
const ProjectAvatar = (props: {
project: Project
project: LocalProject
class?: string
expandable?: boolean
notify?: boolean
@@ -391,7 +380,7 @@ export default function Layout(props: ParentProps) {
)
}
const ProjectVisual = (props: { project: Project & { expanded: boolean }; class?: string }): JSX.Element => {
const ProjectVisual = (props: { project: LocalProject; class?: string }): JSX.Element => {
const name = createMemo(() => getFilename(props.project.worktree))
const current = createMemo(() => base64Decode(params.dir ?? ""))
return (
@@ -427,7 +416,7 @@ export default function Layout(props: ParentProps) {
const SessionItem = (props: {
session: Session
slug: string
project: Project
project: LocalProject
depth?: number
childrenMap: Map<string, Session[]>
}): JSX.Element => {
@@ -517,7 +506,7 @@ export default function Layout(props: ParentProps) {
)
}
const SortableProject = (props: { project: Project & { expanded: boolean } }): JSX.Element => {
const SortableProject = (props: { project: LocalProject }): JSX.Element => {
const sortable = createSortable(props.project.worktree)
const slug = createMemo(() => base64Encode(props.project.worktree))
const name = createMemo(() => getFilename(props.project.worktree))
@@ -542,13 +531,21 @@ export default function Layout(props: ParentProps) {
setProjectStore("limit", (limit) => limit + 5)
await globalSync.project.loadSessions(props.project.worktree)
}
const [expanded, setExpanded] = createSignal(true)
const handleOpenChange = (open: boolean) => {
if (open) layout.projects.expand(props.project.worktree)
else layout.projects.collapse(props.project.worktree)
}
return (
// @ts-ignore
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
<Switch>
<Match when={layout.sidebar.opened()}>
<Collapsible variant="ghost" defaultOpen class="gap-2 shrink-0" onOpenChange={setExpanded}>
<Collapsible
variant="ghost"
open={props.project.expanded}
class="gap-2 shrink-0"
onOpenChange={handleOpenChange}
>
<Button
as={"div"}
variant="ghost"
@@ -559,7 +556,7 @@ export default function Layout(props: ParentProps) {
project={props.project}
class="group-hover/session:hidden"
expandable
notify={!expanded()}
notify={!props.project.expanded}
/>
<span class="truncate text-14-medium text-text-strong">{name()}</span>
</Collapsible.Trigger>
+200 -275
View File
@@ -22,7 +22,6 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Tabs } from "@opencode-ai/ui/tabs"
import { useCodeComponent } from "@opencode-ai/ui/context/code"
@@ -50,7 +49,7 @@ import { DialogSelectFile } from "@/components/dialog-select-file"
import { DialogSelectModel } from "@/components/dialog-select-model"
import { useCommand } from "@/context/command"
import { useNavigate, useParams } from "@solidjs/router"
import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2"
import { UserMessage } from "@opencode-ai/sdk/v2"
import { useSDK } from "@/context/sdk"
import { usePrompt } from "@/context/prompt"
import { extractPromptFromParts } from "@/utils/prompt"
@@ -118,27 +117,8 @@ export default function Page() {
setActiveMessage(msgs[targetIndex])
}
const last = createMemo(
() => messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage,
)
const model = createMemo(() =>
last() ? sync.data.provider.all.find((x) => x.id === last().providerID)?.models[last().modelID] : undefined,
)
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
const tokens = createMemo(() => {
if (!last()) return
const t = last().tokens
return t.input + t.output + t.reasoning + t.cache.read + t.cache.write
})
const context = createMemo(() => {
const total = tokens()
const limit = model()?.limit.context
if (!total || !limit) return 0
return Math.round((total / limit) * 100)
})
const [store, setStore] = createStore({
clickTimer: undefined as number | undefined,
activeDraggable: undefined as string | undefined,
@@ -551,273 +531,218 @@ export default function Page() {
)
}
const wide = createMemo(() => layout.review.state() === "tab" || !diffs().length)
const showTabs = createMemo(() => diffs().length > 0 || tabs().all().length > 0)
return (
<div class="relative bg-background-base size-full overflow-x-hidden flex flex-col">
<div class="min-h-0 grow w-full">
<DragDropProvider
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
collisionDetector={closestCenter}
<div class="min-h-0 grow w-full flex">
{/* Session pane - always visible */}
<div
class="@container relative shrink-0 py-3 flex flex-col gap-6 min-h-0 h-full bg-background-stronger"
style={{ width: showTabs() ? `${layout.session.width()}px` : "100%" }}
>
<DragDropSensors />
<ConstrainDragYAxis />
<Tabs value={tabs().active() ?? "chat"} onChange={tabs().open}>
<div class="sticky top-0 shrink-0 flex">
<Tabs.List>
<Tabs.Trigger value="chat">
<div class="flex gap-x-[17px] items-center">
<div>Session</div>
<Tooltip
value={`${new Intl.NumberFormat("en-US", {
notation: "compact",
compactDisplay: "short",
}).format(tokens() ?? 0)} Tokens`}
class="flex items-center gap-1.5"
>
<ProgressCircle percentage={context() ?? 0} />
<div class="text-14-regular text-text-weak text-left w-7">{context() ?? 0}%</div>
</Tooltip>
</div>
</Tabs.Trigger>
<Show when={layout.review.state() === "tab" && diffs().length}>
<Tabs.Trigger
value="review"
closeButton={
<Tooltip value="Close tab" placement="bottom">
<IconButton icon="collapse" size="normal" variant="ghost" onClick={layout.review.pane} />
</Tooltip>
}
>
<div class="flex items-center gap-3">
<Show when={diffs()}>
<DiffChanges changes={diffs()} variant="bars" />
</Show>
<div class="flex items-center gap-1.5">
<div>Review</div>
<Show when={info()?.summary?.files}>
<div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
{info()?.summary?.files ?? 0}
</div>
</Show>
</div>
</div>
</Tabs.Trigger>
</Show>
<SortableProvider ids={tabs().all() ?? []}>
<For each={tabs().all() ?? []}>
{(tab) => <SortableTab tab={tab} onTabClick={handleTabClick} onTabClose={tabs().close} />}
</For>
</SortableProvider>
<div class="bg-background-base h-full flex items-center justify-center border-b border-border-weak-base px-3">
<Tooltip value="Open file" class="flex items-center">
<IconButton
icon="plus-small"
variant="ghost"
iconSize="large"
onClick={() => dialog.show(() => <DialogSelectFile />)}
<div class="flex-1 min-h-0 overflow-hidden">
<Switch>
<Match when={params.id}>
<div class="flex items-start justify-start h-full min-h-0">
<SessionMessageRail
messages={visibleUserMessages()}
current={activeMessage()}
onMessageSelect={setActiveMessage}
wide={!showTabs()}
/>
<Show when={activeMessage()}>
<SessionTurn
sessionID={params.id!}
messageID={activeMessage()!.id}
stepsExpanded={store.stepsExpanded}
onStepsExpandedToggle={() => setStore("stepsExpanded", (x) => !x)}
onUserInteracted={() => setStore("userInteracted", true)}
classes={{
root: "pb-20 flex-1 min-w-0",
content: "pb-20",
container:
"w-full " +
(!showTabs()
? "max-w-200 mx-auto px-6"
: visibleUserMessages().length > 1
? "pr-6 pl-18"
: "px-6"),
}}
/>
</Tooltip>
</Show>
</div>
</Tabs.List>
</div>
<Tabs.Content
value="chat"
class="@container select-text flex flex-col flex-1 min-h-0 overflow-y-hidden contain-strict"
>
<div
classList={{
"w-full flex-1 min-h-0": true,
grid: layout.review.state() === "tab",
flex: layout.review.state() === "pane",
}}
>
<div
classList={{
"relative shrink-0 py-3 flex flex-col gap-6 flex-1 min-h-0 w-full": true,
"max-w-146 mx-auto": !wide(),
}}
>
<Switch>
<Match when={params.id}>
<div class="flex items-start justify-start h-full min-h-0">
<SessionMessageRail
messages={visibleUserMessages()}
current={activeMessage()}
onMessageSelect={setActiveMessage}
wide={wide()}
/>
<Show when={activeMessage()}>
<SessionTurn
sessionID={params.id!}
messageID={activeMessage()!.id}
stepsExpanded={store.stepsExpanded}
onStepsExpandedToggle={() => setStore("stepsExpanded", (x) => !x)}
onUserInteracted={() => setStore("userInteracted", true)}
classes={{
root: "pb-20 flex-1 min-w-0",
content: "pb-20",
container:
"w-full " +
(wide()
? "max-w-200 mx-auto px-6"
: visibleUserMessages().length > 1
? "pr-6 pl-18"
: "px-6"),
}}
/>
</Show>
</Match>
<Match when={true}>
<div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6">
<div class="text-20-medium text-text-weaker">New session</div>
<div class="flex justify-center items-center gap-3">
<Icon name="folder" size="small" />
<div class="text-12-medium text-text-weak">
{getDirectory(sync.data.path.directory)}
<span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
</div>
</div>
<Show when={sync.project}>
{(project) => (
<div class="flex justify-center items-center gap-3">
<Icon name="pencil-line" size="small" />
<div class="text-12-medium text-text-weak">
Last modified&nbsp;
<span class="text-text-strong">
{DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()}
</span>
</div>
</div>
</Match>
<Match when={true}>
<div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6">
<div class="text-20-medium text-text-weaker">New session</div>
<div class="flex justify-center items-center gap-3">
<Icon name="folder" size="small" />
<div class="text-12-medium text-text-weak">
{getDirectory(sync.data.path.directory)}
<span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
)}
</Show>
</div>
</Match>
</Switch>
</div>
<div class="absolute inset-x-0 bottom-8 flex flex-col justify-center items-center z-50">
<div
classList={{
"w-full px-6": true,
"max-w-200": !showTabs(),
}}
>
<PromptInput
ref={(el) => {
inputRef = el
}}
/>
</div>
</div>
<Show when={showTabs()}>
<ResizeHandle
direction="horizontal"
size={layout.session.width()}
min={450}
max={window.innerWidth * 0.45}
onResize={layout.session.resize}
/>
</Show>
</div>
{/* Tabs pane - visible when there are diffs or file tabs */}
<Show when={showTabs()}>
<div class="relative flex-1 min-w-0 h-full border-l border-border-weak-base">
<DragDropProvider
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
collisionDetector={closestCenter}
>
<DragDropSensors />
<ConstrainDragYAxis />
<Tabs value={tabs().active() ?? "review"} onChange={tabs().open}>
<div class="sticky top-0 shrink-0 flex">
<Tabs.List>
<Show when={diffs().length}>
<Tabs.Trigger value="review">
<div class="flex items-center gap-3">
<Show when={diffs()}>
<DiffChanges changes={diffs()} variant="bars" />
</Show>
<div class="flex items-center gap-1.5">
<div>Review</div>
<Show when={info()?.summary?.files}>
<div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
{info()?.summary?.files ?? 0}
</div>
</Show>
</div>
</div>
<Show when={sync.project}>
{(project) => (
<div class="flex justify-center items-center gap-3">
<Icon name="pencil-line" size="small" />
<div class="text-12-medium text-text-weak">
Last modified&nbsp;
<span class="text-text-strong">
{DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()}
</span>
</div>
</div>
)}
</Show>
</div>
</Match>
</Switch>
<div class="absolute inset-x-0 bottom-8 flex flex-col justify-center items-center z-50">
<div class="w-full max-w-200 px-6">
<PromptInput
ref={(el) => {
inputRef = el
</Tabs.Trigger>
</Show>
<SortableProvider ids={tabs().all() ?? []}>
<For each={tabs().all() ?? []}>
{(tab) => <SortableTab tab={tab} onTabClick={handleTabClick} onTabClose={tabs().close} />}
</For>
</SortableProvider>
<div class="bg-background-base h-full flex items-center justify-center border-b border-border-weak-base px-3">
<Tooltip value="Open file" class="flex items-center">
<IconButton
icon="plus-small"
variant="ghost"
iconSize="large"
onClick={() => dialog.show(() => <DialogSelectFile />)}
/>
</Tooltip>
</div>
</Tabs.List>
</div>
<Show when={diffs().length}>
<Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden contain-strict">
<div class="relative pt-3 flex-1 min-h-0 overflow-hidden">
<SessionReview
classes={{
root: "pb-40",
header: "px-6",
container: "px-6",
}}
diffs={diffs()}
split
/>
</div>
</div>
</div>
<Show when={layout.review.state() === "pane" && diffs().length}>
<div
classList={{
"relative grow pt-3 flex-1 min-h-0 border-l border-border-weak-base contain-strict": true,
}}
>
<SessionReview
classes={{
root: "pb-20",
header: "px-6",
container: "px-6",
}}
diffs={diffs()}
actions={
<Tooltip value="Open in tab">
<IconButton
icon="expand"
variant="ghost"
onClick={() => {
layout.review.tab()
tabs().setActive("review")
}}
/>
</Tooltip>
}
/>
</div>
</Show>
</div>
</Tabs.Content>
<Show when={layout.review.state() === "tab" && diffs().length}>
<Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden contain-strict">
<div
classList={{
"relative pt-3 flex-1 min-h-0 overflow-hidden": true,
}}
>
<SessionReview
classes={{
root: "pb-40",
header: "px-6",
container: "px-6",
}}
diffs={diffs()}
split
/>
</div>
</Tabs.Content>
</Show>
<For each={tabs().all()}>
{(tab) => {
const [file] = createResource(
() => tab,
async (tab) => {
if (tab.startsWith("file://")) {
return local.file.node(tab.replace("file://", ""))
}
return undefined
},
)
return (
<Tabs.Content value={tab} class="select-text mt-3">
<Switch>
<Match when={file()}>
{(f) => (
<Dynamic
component={codeComponent}
file={{
name: f().path,
contents: f().content?.content ?? "",
cacheKey: checksum(f().content?.content ?? ""),
}}
overflow="scroll"
class="pb-40"
/>
)}
</Match>
</Switch>
</Tabs.Content>
)
}}
</For>
</Tabs>
<DragOverlay>
<Show when={store.activeDraggable}>
{(draggedFile) => {
const [file] = createResource(
() => draggedFile(),
async (tab) => {
if (tab.startsWith("file://")) {
return local.file.node(tab.replace("file://", ""))
}
return undefined
},
)
return (
<div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent">
<Show when={file()}>{(f) => <FileVisual active file={f()} />}</Show>
</div>
)
}}
</Show>
</DragOverlay>
</DragDropProvider>
<Show when={tabs().active()}>
<div class="absolute inset-x-0 px-6 max-w-200 flex flex-col justify-center items-center z-50 mx-auto bottom-8">
<PromptInput
ref={(el) => {
inputRef = el
}}
/>
</Show>
<For each={tabs().all()}>
{(tab) => {
const [file] = createResource(
() => tab,
async (tab) => {
if (tab.startsWith("file://")) {
return local.file.node(tab.replace("file://", ""))
}
return undefined
},
)
return (
<Tabs.Content value={tab} class="select-text mt-3">
<Switch>
<Match when={file()}>
{(f) => (
<Dynamic
component={codeComponent}
file={{
name: f().path,
contents: f().content?.content ?? "",
cacheKey: checksum(f().content?.content ?? ""),
}}
overflow="scroll"
class="pb-40"
/>
)}
</Match>
</Switch>
</Tabs.Content>
)
}}
</For>
</Tabs>
<DragOverlay>
<Show when={store.activeDraggable}>
{(draggedFile) => {
const [file] = createResource(
() => draggedFile(),
async (tab) => {
if (tab.startsWith("file://")) {
return local.file.node(tab.replace("file://", ""))
}
return undefined
},
)
return (
<div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent">
<Show when={file()}>{(f) => <FileVisual active file={f()} />}</Show>
</div>
)
}}
</Show>
</DragOverlay>
</DragDropProvider>
</div>
</Show>
</div>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.0.170",
"version": "1.0.181",
"private": true,
"type": "module",
"scripts": {
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.0.170"
version = "1.0.181"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/sst/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.170/opencode-darwin-arm64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.181/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.170/opencode-darwin-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.181/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.170/opencode-linux-arm64.tar.gz"
archive = "https://github.com/sst/opencode/releases/download/v1.0.181/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.170/opencode-linux-x64.tar.gz"
archive = "https://github.com/sst/opencode/releases/download/v1.0.181/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.170/opencode-windows-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.181/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.0.170",
"version": "1.0.181",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.0.170",
"version": "1.0.181",
"name": "opencode",
"type": "module",
"private": true,
+9
View File
@@ -53,6 +53,15 @@ for (const tag of tags) {
}
if (!Script.preview) {
// Create archives for GitHub release
for (const key of Object.keys(binaries)) {
if (key.includes("linux")) {
await $`tar -czf ../../${key}.tar.gz *`.cwd(`dist/${key}/bin`)
} else {
await $`zip -r ../../${key}.zip *`.cwd(`dist/${key}/bin`)
}
}
const image = "ghcr.io/sst/opencode"
const platforms = "linux/amd64,linux/arm64"
const tags = [`${image}:${Script.version}`, `${image}:latest`]
+4 -2
View File
@@ -22,6 +22,7 @@ import { Log } from "../util/log"
import { ACPSessionManager } from "./session"
import type { ACPConfig, ACPSessionState } from "./types"
import { Provider } from "../provider/provider"
import { Agent as AgentModule } from "../agent/agent"
import { Installation } from "@/installation"
import { MessageV2 } from "@/session/message-v2"
import { Config } from "@/config/config"
@@ -705,7 +706,8 @@ export namespace ACP {
description: agent.description,
}))
const currentModeId = availableModes.find((m) => m.name === "build")?.id ?? availableModes[0].id
const defaultAgentName = await AgentModule.defaultAgent()
const currentModeId = availableModes.find((m) => m.name === defaultAgentName)?.id ?? availableModes[0].id
const mcpServers: Record<string, Config.Mcp> = {}
for (const server of params.mcpServers) {
@@ -807,7 +809,7 @@ export namespace ACP {
if (!current) {
this.sessionManager.setModel(session.id, model)
}
const agent = session.modeId ?? "build"
const agent = session.modeId ?? (await AgentModule.defaultAgent())
const parts: Array<
{ type: "text"; text: string } | { type: "file"; url: string; filename: string; mime: string }
+23
View File
@@ -5,6 +5,9 @@ import { generateObject, type ModelMessage } from "ai"
import { SystemPrompt } from "../session/system"
import { Instance } from "../project/instance"
import { mergeDeep } from "remeda"
import { Log } from "../util/log"
const log = Log.create({ service: "agent" })
import PROMPT_GENERATE from "./generate.txt"
import PROMPT_COMPACTION from "./prompt/compaction.txt"
@@ -20,6 +23,7 @@ export namespace Agent {
mode: z.enum(["subagent", "primary", "all"]),
native: z.boolean().optional(),
hidden: z.boolean().optional(),
default: z.boolean().optional(),
topP: z.number().optional(),
temperature: z.number().optional(),
color: z.string().optional(),
@@ -245,6 +249,19 @@ export namespace Agent {
item.permission = mergeAgentPermissions(cfg.permission ?? {}, permission ?? {})
}
}
// Mark the default agent
const defaultName = cfg.default_agent ?? "build"
const defaultCandidate = result[defaultName]
if (defaultCandidate && defaultCandidate.mode !== "subagent") {
defaultCandidate.default = true
} else {
// Fall back to "build" if configured default is invalid
if (result["build"]) {
result["build"].default = true
}
}
return result
})
@@ -256,6 +273,12 @@ export namespace Agent {
return state().then((x) => Object.values(x))
}
export async function defaultAgent(): Promise<string> {
const agents = await state()
const defaultCandidate = Object.values(agents).find((a) => a.default)
return defaultCandidate?.name ?? "build"
}
export async function generate(input: { description: string; model?: { providerID: string; modelID: string } }) {
const cfg = await Config.get()
const defaultModel = input.model ?? (await Provider.defaultModel())
+1 -1
View File
@@ -762,7 +762,7 @@ export const GithubRunCommand = cmd({
providerID,
modelID,
},
agent: "build",
// agent is omitted - server will use default_agent from config or fall back to "build"
parts: [
{
id: Identifier.ascending("part"),
+26 -2
View File
@@ -10,6 +10,7 @@ import { select } from "@clack/prompts"
import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2"
import { Server } from "../../server/server"
import { Provider } from "../../provider/provider"
import { Agent } from "../../agent/agent"
const TOOL: Record<string, [string, string]> = {
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
@@ -223,10 +224,33 @@ export const RunCommand = cmd({
}
})()
// Validate agent if specified
const resolvedAgent = await (async () => {
if (!args.agent) return undefined
const agent = await Agent.get(args.agent)
if (!agent) {
UI.println(
UI.Style.TEXT_WARNING_BOLD + "!",
UI.Style.TEXT_NORMAL,
`agent "${args.agent}" not found. Falling back to default agent`,
)
return undefined
}
if (agent.mode === "subagent") {
UI.println(
UI.Style.TEXT_WARNING_BOLD + "!",
UI.Style.TEXT_NORMAL,
`agent "${args.agent}" is a subagent, not a primary agent. Falling back to default agent`,
)
return undefined
}
return args.agent
})()
if (args.command) {
await sdk.session.command({
sessionID,
agent: args.agent || "build",
agent: resolvedAgent,
model: args.model,
command: args.command,
arguments: message,
@@ -235,7 +259,7 @@ export const RunCommand = cmd({
const modelParam = args.model ? Provider.parseModel(args.model) : undefined
await sdk.session.prompt({
sessionID,
agent: args.agent || "build",
agent: resolvedAgent,
model: modelParam,
parts: [...fileParts, { type: "text", text: message }],
})
+2 -1
View File
@@ -229,7 +229,8 @@ function App() {
let continued = false
createEffect(() => {
if (continued || sync.status !== "complete" || !args.continue) return
// When using -c, session list is loaded in blocking phase, so we can navigate at "partial"
if (continued || sync.status === "loading" || !args.continue) return
const match = sync.data.session
.toSorted((a, b) => b.time.updated - a.time.updated)
.find((x) => x.parentID === undefined)?.id
@@ -56,7 +56,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const [agentStore, setAgentStore] = createStore<{
current: string
}>({
current: agents()[0].name,
current: agents().find((x) => x.default)?.name ?? agents()[0].name,
})
const { theme } = useTheme()
const colors = createMemo(() => [
@@ -10,6 +10,7 @@ export type HomeRoute = {
export type SessionRoute = {
type: "session"
sessionID: string
initialPrompt?: PromptInfo
}
export type Route = HomeRoute | SessionRoute
@@ -22,6 +22,7 @@ import { Binary } from "@opencode-ai/util/binary"
import { createSimpleContext } from "./helper"
import type { Snapshot } from "@/snapshot"
import { useExit } from "./exit"
import { useArgs } from "./args"
import { batch, onMount } from "solid-js"
import { Log } from "@/util/log"
import type { Path } from "@opencode-ai/sdk"
@@ -254,10 +255,18 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
})
const exit = useExit()
const args = useArgs()
async function bootstrap() {
// blocking
await Promise.all([
const sessionListPromise = sdk.client.session.list().then((x) =>
setStore(
"session",
(x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)),
),
)
// blocking - include session.list when continuing a session
const blockingRequests: Promise<unknown>[] = [
sdk.client.config.providers({}, { throwOnError: true }).then((x) => {
batch(() => {
setStore("provider", x.data!.providers)
@@ -271,17 +280,15 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
}),
sdk.client.app.agents({}, { throwOnError: true }).then((x) => setStore("agent", x.data ?? [])),
sdk.client.config.get({}, { throwOnError: true }).then((x) => setStore("config", x.data!)),
])
...(args.continue ? [sessionListPromise] : []),
]
await Promise.all(blockingRequests)
.then(() => {
if (store.status !== "complete") setStore("status", "partial")
// non-blocking
Promise.all([
sdk.client.session.list().then((x) =>
setStore(
"session",
(x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)),
),
),
...(args.continue ? [] : [sessionListPromise]),
sdk.client.command.list().then((x) => setStore("command", x.data ?? [])),
sdk.client.lsp.status().then((x) => setStore("lsp", x.data!)),
sdk.client.mcp.status().then((x) => setStore("mcp", x.data!)),
@@ -6,8 +6,10 @@ import { createSimpleContext } from "./helper"
import aura from "./theme/aura.json" with { type: "json" }
import ayu from "./theme/ayu.json" with { type: "json" }
import catppuccin from "./theme/catppuccin.json" with { type: "json" }
import catppuccinFrappe from "./theme/catppuccin-frappe.json" with { type: "json" }
import catppuccinMacchiato from "./theme/catppuccin-macchiato.json" with { type: "json" }
import cobalt2 from "./theme/cobalt2.json" with { type: "json" }
import cursor from "./theme/cursor.json" with { type: "json" }
import dracula from "./theme/dracula.json" with { type: "json" }
import everforest from "./theme/everforest.json" with { type: "json" }
import flexoki from "./theme/flexoki.json" with { type: "json" }
@@ -136,8 +138,10 @@ export const DEFAULT_THEMES: Record<string, ThemeJson> = {
aura,
ayu,
catppuccin,
["catppuccin-frappe"]: catppuccinFrappe,
["catppuccin-macchiato"]: catppuccinMacchiato,
cobalt2,
cursor,
dracula,
everforest,
flexoki,
@@ -279,14 +283,23 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
ready: false,
})
createEffect(async () => {
const custom = await getCustomThemes()
setStore(
produce((draft) => {
Object.assign(draft.themes, custom)
draft.ready = true
}),
)
createEffect(() => {
getCustomThemes()
.then((custom) => {
setStore(
produce((draft) => {
Object.assign(draft.themes, custom)
}),
)
})
.catch(() => {
setStore("active", "opencode")
})
.finally(() => {
if (store.active !== "system") {
setStore("ready", true)
}
})
})
const renderer = useRenderer()
@@ -295,8 +308,25 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
size: 16,
})
.then((colors) => {
if (!colors.palette[0]) return
setStore("themes", "system", generateSystem(colors, store.mode))
if (!colors.palette[0]) {
if (store.active === "system") {
setStore(
produce((draft) => {
draft.active = "opencode"
draft.ready = true
}),
)
}
return
}
setStore(
produce((draft) => {
draft.themes.system = generateSystem(colors, store.mode)
if (store.active === "system") {
draft.ready = true
}
}),
)
})
const values = createMemo(() => {
@@ -0,0 +1,233 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"frappeRosewater": "#f2d5cf",
"frappeFlamingo": "#eebebe",
"frappePink": "#f4b8e4",
"frappeMauve": "#ca9ee6",
"frappeRed": "#e78284",
"frappeMaroon": "#ea999c",
"frappePeach": "#ef9f76",
"frappeYellow": "#e5c890",
"frappeGreen": "#a6d189",
"frappeTeal": "#81c8be",
"frappeSky": "#99d1db",
"frappeSapphire": "#85c1dc",
"frappeBlue": "#8da4e2",
"frappeLavender": "#babbf1",
"frappeText": "#c6d0f5",
"frappeSubtext1": "#b5bfe2",
"frappeSubtext0": "#a5adce",
"frappeOverlay2": "#949cb8",
"frappeOverlay1": "#838ba7",
"frappeOverlay0": "#737994",
"frappeSurface2": "#626880",
"frappeSurface1": "#51576d",
"frappeSurface0": "#414559",
"frappeBase": "#303446",
"frappeMantle": "#292c3c",
"frappeCrust": "#232634"
},
"theme": {
"primary": {
"dark": "frappeBlue",
"light": "frappeBlue"
},
"secondary": {
"dark": "frappeMauve",
"light": "frappeMauve"
},
"accent": {
"dark": "frappePink",
"light": "frappePink"
},
"error": {
"dark": "frappeRed",
"light": "frappeRed"
},
"warning": {
"dark": "frappeYellow",
"light": "frappeYellow"
},
"success": {
"dark": "frappeGreen",
"light": "frappeGreen"
},
"info": {
"dark": "frappeTeal",
"light": "frappeTeal"
},
"text": {
"dark": "frappeText",
"light": "frappeText"
},
"textMuted": {
"dark": "frappeSubtext1",
"light": "frappeSubtext1"
},
"background": {
"dark": "frappeBase",
"light": "frappeBase"
},
"backgroundPanel": {
"dark": "frappeMantle",
"light": "frappeMantle"
},
"backgroundElement": {
"dark": "frappeCrust",
"light": "frappeCrust"
},
"border": {
"dark": "frappeSurface0",
"light": "frappeSurface0"
},
"borderActive": {
"dark": "frappeSurface1",
"light": "frappeSurface1"
},
"borderSubtle": {
"dark": "frappeSurface2",
"light": "frappeSurface2"
},
"diffAdded": {
"dark": "frappeGreen",
"light": "frappeGreen"
},
"diffRemoved": {
"dark": "frappeRed",
"light": "frappeRed"
},
"diffContext": {
"dark": "frappeOverlay2",
"light": "frappeOverlay2"
},
"diffHunkHeader": {
"dark": "frappePeach",
"light": "frappePeach"
},
"diffHighlightAdded": {
"dark": "frappeGreen",
"light": "frappeGreen"
},
"diffHighlightRemoved": {
"dark": "frappeRed",
"light": "frappeRed"
},
"diffAddedBg": {
"dark": "#29342b",
"light": "#29342b"
},
"diffRemovedBg": {
"dark": "#3a2a31",
"light": "#3a2a31"
},
"diffContextBg": {
"dark": "frappeMantle",
"light": "frappeMantle"
},
"diffLineNumber": {
"dark": "frappeSurface1",
"light": "frappeSurface1"
},
"diffAddedLineNumberBg": {
"dark": "#223025",
"light": "#223025"
},
"diffRemovedLineNumberBg": {
"dark": "#2f242b",
"light": "#2f242b"
},
"markdownText": {
"dark": "frappeText",
"light": "frappeText"
},
"markdownHeading": {
"dark": "frappeMauve",
"light": "frappeMauve"
},
"markdownLink": {
"dark": "frappeBlue",
"light": "frappeBlue"
},
"markdownLinkText": {
"dark": "frappeSky",
"light": "frappeSky"
},
"markdownCode": {
"dark": "frappeGreen",
"light": "frappeGreen"
},
"markdownBlockQuote": {
"dark": "frappeYellow",
"light": "frappeYellow"
},
"markdownEmph": {
"dark": "frappeYellow",
"light": "frappeYellow"
},
"markdownStrong": {
"dark": "frappePeach",
"light": "frappePeach"
},
"markdownHorizontalRule": {
"dark": "frappeSubtext0",
"light": "frappeSubtext0"
},
"markdownListItem": {
"dark": "frappeBlue",
"light": "frappeBlue"
},
"markdownListEnumeration": {
"dark": "frappeSky",
"light": "frappeSky"
},
"markdownImage": {
"dark": "frappeBlue",
"light": "frappeBlue"
},
"markdownImageText": {
"dark": "frappeSky",
"light": "frappeSky"
},
"markdownCodeBlock": {
"dark": "frappeText",
"light": "frappeText"
},
"syntaxComment": {
"dark": "frappeOverlay2",
"light": "frappeOverlay2"
},
"syntaxKeyword": {
"dark": "frappeMauve",
"light": "frappeMauve"
},
"syntaxFunction": {
"dark": "frappeBlue",
"light": "frappeBlue"
},
"syntaxVariable": {
"dark": "frappeRed",
"light": "frappeRed"
},
"syntaxString": {
"dark": "frappeGreen",
"light": "frappeGreen"
},
"syntaxNumber": {
"dark": "frappePeach",
"light": "frappePeach"
},
"syntaxType": {
"dark": "frappeYellow",
"light": "frappeYellow"
},
"syntaxOperator": {
"dark": "frappeSky",
"light": "frappeSky"
},
"syntaxPunctuation": {
"dark": "frappeText",
"light": "frappeText"
}
}
}
@@ -0,0 +1,249 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"darkBg": "#181818",
"darkPanel": "#141414",
"darkElement": "#262626",
"darkFg": "#e4e4e4",
"darkMuted": "#e4e4e45e",
"darkBorder": "#e4e4e413",
"darkBorderActive": "#e4e4e426",
"darkCyan": "#88c0d0",
"darkBlue": "#81a1c1",
"darkGreen": "#3fa266",
"darkGreenBright": "#70b489",
"darkRed": "#e34671",
"darkRedBright": "#fc6b83",
"darkYellow": "#f1b467",
"darkOrange": "#d2943e",
"darkPink": "#E394DC",
"darkPurple": "#AAA0FA",
"darkTeal": "#82D2CE",
"darkSyntaxYellow": "#F8C762",
"darkSyntaxOrange": "#EFB080",
"darkSyntaxGreen": "#A8CC7C",
"darkSyntaxBlue": "#87C3FF",
"lightBg": "#fcfcfc",
"lightPanel": "#f3f3f3",
"lightElement": "#ededed",
"lightFg": "#141414",
"lightMuted": "#141414ad",
"lightBorder": "#14141413",
"lightBorderActive": "#14141426",
"lightTeal": "#6f9ba6",
"lightBlue": "#3c7cab",
"lightBlueDark": "#206595",
"lightGreen": "#1f8a65",
"lightGreenBright": "#55a583",
"lightRed": "#cf2d56",
"lightRedBright": "#e75e78",
"lightOrange": "#db704b",
"lightYellow": "#c08532",
"lightPurple": "#9e94d5",
"lightPurpleDark": "#6049b3",
"lightPink": "#b8448b",
"lightMagenta": "#b3003f"
},
"theme": {
"primary": {
"dark": "darkCyan",
"light": "lightTeal"
},
"secondary": {
"dark": "darkBlue",
"light": "lightBlue"
},
"accent": {
"dark": "darkCyan",
"light": "lightTeal"
},
"error": {
"dark": "darkRed",
"light": "lightRed"
},
"warning": {
"dark": "darkYellow",
"light": "lightOrange"
},
"success": {
"dark": "darkGreen",
"light": "lightGreen"
},
"info": {
"dark": "darkBlue",
"light": "lightBlue"
},
"text": {
"dark": "darkFg",
"light": "lightFg"
},
"textMuted": {
"dark": "darkMuted",
"light": "lightMuted"
},
"background": {
"dark": "darkBg",
"light": "lightBg"
},
"backgroundPanel": {
"dark": "darkPanel",
"light": "lightPanel"
},
"backgroundElement": {
"dark": "darkElement",
"light": "lightElement"
},
"border": {
"dark": "darkBorder",
"light": "lightBorder"
},
"borderActive": {
"dark": "darkCyan",
"light": "lightTeal"
},
"borderSubtle": {
"dark": "#0f0f0f",
"light": "#e0e0e0"
},
"diffAdded": {
"dark": "darkGreen",
"light": "lightGreen"
},
"diffRemoved": {
"dark": "darkRed",
"light": "lightRed"
},
"diffContext": {
"dark": "darkMuted",
"light": "lightMuted"
},
"diffHunkHeader": {
"dark": "darkMuted",
"light": "lightMuted"
},
"diffHighlightAdded": {
"dark": "darkGreenBright",
"light": "lightGreenBright"
},
"diffHighlightRemoved": {
"dark": "darkRedBright",
"light": "lightRedBright"
},
"diffAddedBg": {
"dark": "#3fa26633",
"light": "#1f8a651f"
},
"diffRemovedBg": {
"dark": "#b8004933",
"light": "#cf2d5614"
},
"diffContextBg": {
"dark": "darkPanel",
"light": "lightPanel"
},
"diffLineNumber": {
"dark": "#e4e4e442",
"light": "#1414147a"
},
"diffAddedLineNumberBg": {
"dark": "#3fa26633",
"light": "#1f8a651f"
},
"diffRemovedLineNumberBg": {
"dark": "#b8004933",
"light": "#cf2d5614"
},
"markdownText": {
"dark": "darkFg",
"light": "lightFg"
},
"markdownHeading": {
"dark": "darkPurple",
"light": "lightBlueDark"
},
"markdownLink": {
"dark": "darkTeal",
"light": "lightBlueDark"
},
"markdownLinkText": {
"dark": "darkBlue",
"light": "lightMuted"
},
"markdownCode": {
"dark": "darkPink",
"light": "lightGreen"
},
"markdownBlockQuote": {
"dark": "darkMuted",
"light": "lightMuted"
},
"markdownEmph": {
"dark": "darkTeal",
"light": "lightFg"
},
"markdownStrong": {
"dark": "darkSyntaxYellow",
"light": "lightFg"
},
"markdownHorizontalRule": {
"dark": "darkMuted",
"light": "lightMuted"
},
"markdownListItem": {
"dark": "darkFg",
"light": "lightFg"
},
"markdownListEnumeration": {
"dark": "darkCyan",
"light": "lightMuted"
},
"markdownImage": {
"dark": "darkCyan",
"light": "lightBlueDark"
},
"markdownImageText": {
"dark": "darkBlue",
"light": "lightMuted"
},
"markdownCodeBlock": {
"dark": "darkFg",
"light": "lightFg"
},
"syntaxComment": {
"dark": "darkMuted",
"light": "lightMuted"
},
"syntaxKeyword": {
"dark": "darkTeal",
"light": "lightMagenta"
},
"syntaxFunction": {
"dark": "darkSyntaxOrange",
"light": "lightOrange"
},
"syntaxVariable": {
"dark": "darkFg",
"light": "lightFg"
},
"syntaxString": {
"dark": "darkPink",
"light": "lightPurple"
},
"syntaxNumber": {
"dark": "darkSyntaxYellow",
"light": "lightPink"
},
"syntaxType": {
"dark": "darkSyntaxOrange",
"light": "lightBlueDark"
},
"syntaxOperator": {
"dark": "darkFg",
"light": "lightFg"
},
"syntaxPunctuation": {
"dark": "darkFg",
"light": "lightFg"
}
}
}
@@ -6,6 +6,7 @@ import { Locale } from "@/util/locale"
import { useSDK } from "@tui/context/sdk"
import { useRoute } from "@tui/context/route"
import { useDialog } from "../../ui/dialog"
import type { PromptInfo } from "@tui/component/prompt/history"
export function DialogForkFromTimeline(props: { sessionID: string; onMove: (messageID: string) => void }) {
const sync = useSync()
@@ -35,9 +36,21 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess
sessionID: props.sessionID,
messageID: message.id,
})
const parts = sync.data.part[message.id] ?? []
const initialPrompt = parts.reduce(
(agg, part) => {
if (part.type === "text") {
if (!part.synthetic) agg.input += part.text
}
if (part.type === "file") agg.parts.push(part)
return agg
},
{ input: "", parts: [] as PromptInfo["parts"] },
)
route.navigate({
sessionID: forked.data!.id,
type: "session",
initialPrompt,
})
dialog.clear()
},
@@ -80,9 +80,25 @@ export function DialogMessage(props: {
sessionID: props.sessionID,
messageID: props.messageID,
})
const initialPrompt = (() => {
const msg = message()
if (!msg) return undefined
const parts = sync.data.part[msg.id]
return parts.reduce(
(agg, part) => {
if (part.type === "text") {
if (!part.synthetic) agg.input += part.text
}
if (part.type === "file") agg.parts.push(part)
return agg
},
{ input: "", parts: [] as PromptInfo["parts"] },
)
})()
route.navigate({
sessionID: result.data!.id,
type: "session",
initialPrompt,
})
dialog.clear()
},
@@ -168,6 +168,13 @@ export function Session() {
const toast = useToast()
const sdk = useSDK()
// Handle initial prompt from fork
createEffect(() => {
if (route.initialPrompt && prompt) {
prompt.set(route.initialPrompt)
}
})
// Auto-navigate to whichever session currently needs permission input
createEffect(() => {
const currentSession = session()
+2 -1
View File
@@ -32,7 +32,8 @@ export function FormatError(input: unknown) {
}
if (Config.InvalidError.isInstance(input))
return [
`Config file at ${input.data.path} is invalid` + (input.data.message ? `: ${input.data.message}` : ""),
`Configuration is invalid${input.data.path && input.data.path !== "config" ? ` at ${input.data.path}` : ""}` +
(input.data.message ? `: ${input.data.message}` : ""),
...(input.data.issues?.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) ?? []),
].join("\n")
+17
View File
@@ -141,6 +141,17 @@ export namespace Config {
if (!result.keybinds) result.keybinds = Info.shape.keybinds.parse({})
// Only validate if user has configured agents - if none configured, built-in agents will be used
if (Object.keys(result.agent).length > 0) {
const primaryAgents = Object.values(result.agent).filter((a) => a.mode !== "subagent" && !a.hidden && !a.disable)
if (primaryAgents.length === 0) {
throw new InvalidError({
path: "config",
message: "No primary agents are available. Please configure at least one agent with mode 'primary' or 'all'.",
})
}
}
return {
config: result,
directories,
@@ -666,6 +677,12 @@ export namespace Config {
.string()
.describe("Small model to use for tasks like title generation in the format of provider/model")
.optional(),
default_agent: z
.string()
.optional()
.describe(
"Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.",
),
username: z
.string()
.optional()
+6 -5
View File
@@ -1,5 +1,4 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import z from "zod"
import { $ } from "bun"
import type { BunFile } from "bun"
@@ -74,6 +73,7 @@ export namespace File {
async function shouldEncode(file: BunFile): Promise<boolean> {
const type = file.type?.toLowerCase()
log.info("shouldEncode", { type })
if (!type) return false
if (type.startsWith("text/")) return false
@@ -93,7 +93,6 @@ export namespace File {
"bzip",
"compressed",
"binary",
"stream",
"pdf",
"msword",
"powerpoint",
@@ -290,9 +289,11 @@ export namespace File {
}
const resolved = dir ? path.join(Instance.directory, dir) : Instance.directory
const nodes: Node[] = []
for (const entry of await fs.promises.readdir(resolved, {
withFileTypes: true,
})) {
for (const entry of await fs.promises
.readdir(resolved, {
withFileTypes: true,
})
.catch(() => [])) {
if (exclude.includes(entry.name)) continue
const fullPath = path.join(resolved, entry.name)
const relativePath = path.relative(Instance.directory, fullPath)
@@ -424,6 +424,10 @@ export namespace ProviderTransform {
result.required = result.required.filter((field: any) => field in result.properties)
}
if (result.type === "array" && result.items == null) {
result.items = {}
}
return result
}
+75 -2
View File
@@ -1060,11 +1060,11 @@ export namespace Server {
const sessionID = c.req.valid("param").sessionID
const body = c.req.valid("json")
const msgs = await Session.messages({ sessionID })
let currentAgent = "build"
let currentAgent = await Agent.defaultAgent()
for (let i = msgs.length - 1; i >= 0; i--) {
const info = msgs[i].info
if (info.role === "user") {
currentAgent = info.agent || "build"
currentAgent = info.agent || (await Agent.defaultAgent())
break
}
}
@@ -1188,6 +1188,79 @@ export namespace Server {
return c.json(message)
},
)
.delete(
"/session/:sessionID/message/:messageID/part/:partID",
describeRoute({
description: "Delete a part from a message",
operationId: "part.delete",
responses: {
200: {
description: "Successfully deleted part",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(400, 404),
},
}),
validator(
"param",
z.object({
sessionID: z.string().meta({ description: "Session ID" }),
messageID: z.string().meta({ description: "Message ID" }),
partID: z.string().meta({ description: "Part ID" }),
}),
),
async (c) => {
const params = c.req.valid("param")
await Session.removePart({
sessionID: params.sessionID,
messageID: params.messageID,
partID: params.partID,
})
return c.json(true)
},
)
.patch(
"/session/:sessionID/message/:messageID/part/:partID",
describeRoute({
description: "Update a part in a message",
operationId: "part.update",
responses: {
200: {
description: "Successfully updated part",
content: {
"application/json": {
schema: resolver(MessageV2.Part),
},
},
},
...errors(400, 404),
},
}),
validator(
"param",
z.object({
sessionID: z.string().meta({ description: "Session ID" }),
messageID: z.string().meta({ description: "Message ID" }),
partID: z.string().meta({ description: "Part ID" }),
}),
),
validator("json", MessageV2.Part),
async (c) => {
const params = c.req.valid("param")
const body = c.req.valid("json")
if (body.id !== params.partID || body.messageID !== params.messageID || body.sessionID !== params.sessionID) {
throw new Error(
`Part mismatch: body.id='${body.id}' vs partID='${params.partID}', body.messageID='${body.messageID}' vs messageID='${params.messageID}', body.sessionID='${body.sessionID}' vs sessionID='${params.sessionID}'`,
)
}
const part = await Session.updatePart(body)
return c.json(part)
},
)
.post(
"/session/:sessionID/message",
describeRoute({
+17
View File
@@ -339,6 +339,23 @@ export namespace Session {
},
)
export const removePart = fn(
z.object({
sessionID: Identifier.schema("session"),
messageID: Identifier.schema("message"),
partID: Identifier.schema("part"),
}),
async (input) => {
await Storage.remove(["part", input.messageID, input.partID])
Bus.publish(MessageV2.Event.PartRemoved, {
sessionID: input.sessionID,
messageID: input.messageID,
partID: input.partID,
})
return input.partID
},
)
const UpdatePartInput = z.union([
MessageV2.Part,
z.object({
+3 -3
View File
@@ -715,7 +715,7 @@ export namespace SessionPrompt {
}
async function createUserMessage(input: PromptInput) {
const agent = await Agent.get(input.agent ?? "build")
const agent = await Agent.get(input.agent ?? (await Agent.defaultAgent()))
const info: MessageV2.Info = {
id: input.messageID ?? Identifier.ascending("message"),
role: "user",
@@ -1282,7 +1282,7 @@ export namespace SessionPrompt {
export async function command(input: CommandInput) {
log.info("command", input)
const command = await Command.get(input.command)
const agentName = command.agent ?? input.agent ?? "build"
const agentName = command.agent ?? input.agent ?? (await Agent.defaultAgent())
const raw = input.arguments.match(argsRegex) ?? []
const args = raw.map((arg) => arg.replace(quoteTrimRegex, ""))
@@ -1425,7 +1425,7 @@ export namespace SessionPrompt {
time: {
created: Date.now(),
},
agent: input.message.info.role === "user" ? input.message.info.agent : "build",
agent: input.message.info.role === "user" ? input.message.info.agent : await Agent.defaultAgent(),
model: {
providerID: input.providerID,
modelID: input.modelID,
@@ -167,6 +167,30 @@ describe("ProviderTransform.maxOutputTokens", () => {
})
})
describe("ProviderTransform.schema - gemini array items", () => {
test("adds missing items for array properties", () => {
const geminiModel = {
providerID: "google",
api: {
id: "gemini-3-pro",
},
} as any
const schema = {
type: "object",
properties: {
nodes: { type: "array" },
edges: { type: "array", items: { type: "string" } },
},
} as any
const result = ProviderTransform.schema(geminiModel, schema) as any
expect(result.properties.nodes.items).toBeDefined()
expect(result.properties.edges.items.type).toBe("string")
})
})
describe("ProviderTransform.message - DeepSeek reasoning content", () => {
test("DeepSeek with tool calls includes reasoning_content in providerOptions", () => {
const msgs = [
+2 -2
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.0.170",
"version": "1.0.181",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
@@ -24,4 +24,4 @@
"typescript": "catalog:",
"@typescript/native-preview": "catalog:"
}
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.0.170",
"version": "1.0.181",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
@@ -29,4 +29,4 @@
"publishConfig": {
"directory": "dist"
}
}
}
+80
View File
@@ -47,6 +47,11 @@ import type {
McpLocalConfig,
McpRemoteConfig,
McpStatusResponses,
Part as Part2,
PartDeleteErrors,
PartDeleteResponses,
PartUpdateErrors,
PartUpdateResponses,
PathGetResponses,
PermissionRespondErrors,
PermissionRespondResponses,
@@ -1486,6 +1491,79 @@ export class Session extends HeyApiClient {
}
}
export class Part extends HeyApiClient {
/**
* Delete a part from a message
*/
public delete<ThrowOnError extends boolean = false>(
parameters: {
sessionID: string
messageID: string
partID: string
directory?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "sessionID" },
{ in: "path", key: "messageID" },
{ in: "path", key: "partID" },
{ in: "query", key: "directory" },
],
},
],
)
return (options?.client ?? this.client).delete<PartDeleteResponses, PartDeleteErrors, ThrowOnError>({
url: "/session/{sessionID}/message/{messageID}/part/{partID}",
...options,
...params,
})
}
/**
* Update a part in a message
*/
public update<ThrowOnError extends boolean = false>(
parameters: {
sessionID: string
messageID: string
partID: string
directory?: string
part?: Part2
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "sessionID" },
{ in: "path", key: "messageID" },
{ in: "path", key: "partID" },
{ in: "query", key: "directory" },
{ key: "part", map: "body" },
],
},
],
)
return (options?.client ?? this.client).patch<PartUpdateResponses, PartUpdateErrors, ThrowOnError>({
url: "/session/{sessionID}/message/{messageID}/part/{partID}",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
}
export class Permission extends HeyApiClient {
/**
* Respond to permission
@@ -2588,6 +2666,8 @@ export class OpencodeClient extends HeyApiClient {
session = new Session({ client: this.client })
part = new Part({ client: this.client })
permission = new Permission({ client: this.client })
command = new Command({ client: this.client })
+93
View File
@@ -1414,6 +1414,10 @@ export type Config = {
* Small model to use for tasks like title generation in the format of provider/model
*/
small_model?: string
/**
* Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.
*/
default_agent?: string
/**
* Custom username to display in conversations instead of system username
*/
@@ -1767,6 +1771,7 @@ export type Agent = {
mode: "subagent" | "primary" | "all"
native?: boolean
hidden?: boolean
default?: boolean
topP?: number
temperature?: number
color?: string
@@ -2915,6 +2920,94 @@ export type SessionMessageResponses = {
export type SessionMessageResponse = SessionMessageResponses[keyof SessionMessageResponses]
export type PartDeleteData = {
body?: never
path: {
/**
* Session ID
*/
sessionID: string
/**
* Message ID
*/
messageID: string
/**
* Part ID
*/
partID: string
}
query?: {
directory?: string
}
url: "/session/{sessionID}/message/{messageID}/part/{partID}"
}
export type PartDeleteErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type PartDeleteError = PartDeleteErrors[keyof PartDeleteErrors]
export type PartDeleteResponses = {
/**
* Successfully deleted part
*/
200: boolean
}
export type PartDeleteResponse = PartDeleteResponses[keyof PartDeleteResponses]
export type PartUpdateData = {
body?: Part
path: {
/**
* Session ID
*/
sessionID: string
/**
* Message ID
*/
messageID: string
/**
* Part ID
*/
partID: string
}
query?: {
directory?: string
}
url: "/session/{sessionID}/message/{messageID}/part/{partID}"
}
export type PartUpdateErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type PartUpdateError = PartUpdateErrors[keyof PartUpdateErrors]
export type PartUpdateResponses = {
/**
* Successfully updated part
*/
200: Part
}
export type PartUpdateResponse = PartUpdateResponses[keyof PartUpdateResponses]
export type SessionPromptAsyncData = {
body?: {
messageID?: string
+174
View File
@@ -2126,6 +2126,173 @@
]
}
},
"/session/{sessionID}/message/{messageID}/part/{partID}": {
"delete": {
"operationId": "part.delete",
"parameters": [
{
"in": "query",
"name": "directory",
"schema": {
"type": "string"
}
},
{
"in": "path",
"name": "sessionID",
"schema": {
"type": "string"
},
"required": true,
"description": "Session ID"
},
{
"in": "path",
"name": "messageID",
"schema": {
"type": "string"
},
"required": true,
"description": "Message ID"
},
{
"in": "path",
"name": "partID",
"schema": {
"type": "string"
},
"required": true,
"description": "Part ID"
}
],
"description": "Delete a part from a message",
"responses": {
"200": {
"description": "Successfully deleted part",
"content": {
"application/json": {
"schema": {
"type": "boolean"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NotFoundError"
}
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.part.delete({\n ...\n})"
}
]
},
"patch": {
"operationId": "part.update",
"parameters": [
{
"in": "query",
"name": "directory",
"schema": {
"type": "string"
}
},
{
"in": "path",
"name": "sessionID",
"schema": {
"type": "string"
},
"required": true,
"description": "Session ID"
},
{
"in": "path",
"name": "messageID",
"schema": {
"type": "string"
},
"required": true,
"description": "Message ID"
},
{
"in": "path",
"name": "partID",
"schema": {
"type": "string"
},
"required": true,
"description": "Part ID"
}
],
"description": "Update a part in a message",
"responses": {
"200": {
"description": "Successfully updated part",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Part"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NotFoundError"
}
}
}
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Part"
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.part.update({\n ...\n})"
}
]
}
},
"/session/{sessionID}/prompt_async": {
"post": {
"operationId": "session.prompt_async",
@@ -7986,6 +8153,10 @@
"description": "Small model to use for tasks like title generation in the format of provider/model",
"type": "string"
},
"default_agent": {
"description": "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.",
"type": "string"
},
"username": {
"description": "Custom username to display in conversations instead of system username",
"type": "string"
@@ -8985,6 +9156,9 @@
"hidden": {
"type": "boolean"
},
"default": {
"type": "boolean"
},
"topP": {
"type": "number"
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.0.170",
"version": "1.0.181",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/tauri",
"private": true,
"version": "1.0.170",
"version": "1.0.181",
"type": "module",
"scripts": {
"typecheck": "tsgo -b",
+2 -1
View File
@@ -3,13 +3,14 @@
import { $ } from "bun"
import { copyBinaryToSidecarFolder, getCurrentSidecar } from "./utils"
import { Script } from "@opencode-ai/script"
const sidecarConfig = getCurrentSidecar()
const dir = "src-tauri/target/opencode-binaries"
await $`mkdir -p ${dir}`
await $`gh release download ${Bun.env.OPENCODE_RELEASE_TAG} --pattern ${sidecarConfig.ocBinary}.${sidecarConfig.assetExt} --repo sst/opencode --skip-existing --dir ${dir}`
await $`gh release download v${Script.version} --pattern ${sidecarConfig.ocBinary}.${sidecarConfig.assetExt} --repo sst/opencode --skip-existing --dir ${dir}`
if (sidecarConfig.assetExt === "tar.gz") {
await $`tar -xvzf ${dir}/${sidecarConfig.ocBinary}.${sidecarConfig.assetExt} -C ${dir}`
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.0.170",
"version": "1.0.181",
"type": "module",
"exports": {
"./*": "./src/components/*.tsx",
@@ -1,4 +1,5 @@
[data-component="tool-trigger"] {
content-visibility: auto;
width: 100%;
display: flex;
align-items: center;
+4 -5
View File
@@ -1,4 +1,4 @@
import { children, For, Match, Show, Switch, type JSX } from "solid-js"
import { For, Match, Show, Switch, type JSX } from "solid-js"
import { Collapsible } from "./collapsible"
import { Icon, IconProps } from "./icon"
@@ -27,7 +27,6 @@ export interface BasicToolProps {
}
export function BasicTool(props: BasicToolProps) {
const resolved = children(() => props.children)
return (
<Collapsible defaultOpen={props.defaultOpen}>
<Collapsible.Trigger>
@@ -81,13 +80,13 @@ export function BasicTool(props: BasicToolProps) {
</Switch>
</div>
</div>
<Show when={resolved() && !props.hideDetails}>
<Show when={props.children && !props.hideDetails}>
<Collapsible.Arrow />
</Show>
</div>
</Collapsible.Trigger>
<Show when={resolved() && !props.hideDetails}>
<Collapsible.Content>{resolved()}</Collapsible.Content>
<Show when={props.children && !props.hideDetails}>
<Collapsible.Content>{props.children}</Collapsible.Content>
</Show>
</Collapsible>
)
+3 -4
View File
@@ -1,5 +1,5 @@
import { Checkbox as Kobalte } from "@kobalte/core/checkbox"
import { children, Show, splitProps } from "solid-js"
import { Show, splitProps } from "solid-js"
import type { ComponentProps, JSX, ParentProps } from "solid-js"
export interface CheckboxProps extends ParentProps<ComponentProps<typeof Kobalte>> {
@@ -10,7 +10,6 @@ export interface CheckboxProps extends ParentProps<ComponentProps<typeof Kobalte
export function Checkbox(props: CheckboxProps) {
const [local, others] = splitProps(props, ["children", "class", "label", "hideLabel", "description", "icon"])
const resolved = children(() => local.children)
return (
<Kobalte {...others} data-component="checkbox">
<Kobalte.Input data-slot="checkbox-checkbox-input" />
@@ -29,9 +28,9 @@ export function Checkbox(props: CheckboxProps) {
</Kobalte.Indicator>
</Kobalte.Control>
<div data-slot="checkbox-checkbox-content">
<Show when={resolved()}>
<Show when={props.children}>
<Kobalte.Label data-slot="checkbox-checkbox-label" classList={{ "sr-only": local.hideLabel }}>
{resolved()}
{props.children}
</Kobalte.Label>
</Show>
<Show when={local.description}>
+1
View File
@@ -1,3 +1,4 @@
[data-component="code"] {
content-visibility: auto;
overflow: hidden;
}
+1 -1
View File
@@ -1,5 +1,5 @@
[data-component="diff"] {
contain: content;
content-visibility: auto;
[data-slot="diff-hunk-separator-line-number"] {
position: sticky;
@@ -1,4 +1,5 @@
[data-component="assistant-message"] {
content-visibility: auto;
width: 100%;
display: flex;
flex-direction: column;
+7 -7
View File
@@ -463,13 +463,13 @@ ToolRegistry.register({
subtitle: props.input.description,
}}
>
<Show when={props.output}>
{(output) => (
<div data-component="tool-output" data-scrollable>
<Markdown text={output()} />
</div>
)}
</Show>
{/* <Show when={false && props.output}> */}
{/* {(output) => ( */}
{/* <div data-component="tool-output" data-scrollable> */}
{/* <Markdown text={output()} /> */}
{/* </div> */}
{/* )} */}
{/* </Show> */}
</BasicTool>
)
},
@@ -25,7 +25,7 @@ export interface SessionReviewProps {
export const SessionReview = (props: SessionReviewProps) => {
const diffComponent = useDiffComponent()
const [store, setStore] = createStore({
open: props.diffs.map((d) => d.file),
open: props.diffs.length > 10 ? [] : props.diffs.map((d) => d.file),
})
const handleChange = (open: string[]) => {
@@ -78,7 +78,7 @@ export const SessionReview = (props: SessionReviewProps) => {
<Accordion multiple value={store.open} onChange={handleChange}>
<For each={props.diffs}>
{(diff) => (
<Accordion.Item forceMount value={diff.file} data-slot="session-review-accordion-item">
<Accordion.Item value={diff.file} data-slot="session-review-accordion-item">
<StickyAccordionHeader>
<Accordion.Trigger>
<div data-slot="session-review-trigger-content">
+1 -1
View File
@@ -499,7 +499,7 @@ export function SessionTurn(
)
return (
<Switch>
<Match when={response() && lastTextPart()?.id === last()?.id}>
<Match when={!summary() && response() && lastTextPart()?.id === last()?.id}>
<Message
message={assistantMessage}
parts={parts().filter((p) => p?.id !== last()?.id)}
+3 -4
View File
@@ -1,5 +1,5 @@
import { Switch as Kobalte } from "@kobalte/core/switch"
import { children, Show, splitProps } from "solid-js"
import { Show, splitProps } from "solid-js"
import type { ComponentProps, ParentProps } from "solid-js"
export interface SwitchProps extends ParentProps<ComponentProps<typeof Kobalte>> {
@@ -9,13 +9,12 @@ export interface SwitchProps extends ParentProps<ComponentProps<typeof Kobalte>>
export function Switch(props: SwitchProps) {
const [local, others] = splitProps(props, ["children", "class", "hideLabel", "description"])
const resolved = children(() => local.children)
return (
<Kobalte {...others} data-component="switch">
<Kobalte.Input data-slot="switch-input" />
<Show when={resolved()}>
<Show when={local.children}>
<Kobalte.Label data-slot="switch-label" classList={{ "sr-only": local.hideLabel }}>
{resolved()}
{local.children}
</Kobalte.Label>
</Show>
<Show when={local.description}>
+8 -1
View File
@@ -24,6 +24,13 @@ type Data = {
export const { use: useData, provider: DataProvider } = createSimpleContext({
name: "Data",
init: (props: { data: Data; directory: string }) => {
return { store: props.data, directory: props.directory }
return {
get store() {
return props.data
},
get directory() {
return props.directory
},
}
},
})
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/util",
"version": "1.0.170",
"version": "1.0.181",
"private": true,
"type": "module",
"exports": {
+41
View File
@@ -0,0 +1,41 @@
export interface RetryOptions {
attempts?: number
delay?: number
factor?: number
maxDelay?: number
retryIf?: (error: unknown) => boolean
}
const TRANSIENT_MESSAGES = [
"load failed",
"network connection was lost",
"network request failed",
"failed to fetch",
"econnreset",
"econnrefused",
"etimedout",
"socket hang up",
]
function isTransientError(error: unknown): boolean {
if (!error) return false
const message = String(error instanceof Error ? error.message : error).toLowerCase()
return TRANSIENT_MESSAGES.some((m) => message.includes(m))
}
export async function retry<T>(fn: () => Promise<T>, options: RetryOptions = {}): Promise<T> {
const { attempts = 3, delay = 500, factor = 2, maxDelay = 10000, retryIf = isTransientError } = options
let lastError: unknown
for (let attempt = 0; attempt < attempts; attempt++) {
try {
return await fn()
} catch (error) {
lastError = error
if (attempt === attempts - 1 || !retryIf(error)) throw error
const wait = Math.min(delay * Math.pow(factor, attempt), maxDelay)
await new Promise((resolve) => setTimeout(resolve, wait))
}
}
throw lastError
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/web",
"type": "module",
"version": "1.0.170",
"version": "1.0.181",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
+2
View File
@@ -331,6 +331,8 @@ If you dont specify a model, primary agents use the [model globally configure
}
```
The model ID in your OpenCode config uses the format `provider/model-id`. For example, if you're using [OpenCode Zen](/docs/zen), you would use `opencode/gpt-5.1-codex` for GPT 5.1 Codex.
---
### Tools
+17
View File
@@ -194,6 +194,23 @@ You can also define agents using markdown files in `~/.config/opencode/agent/` o
---
### Default agent
You can set the default agent using the `default_agent` option. This determines which agent is used when none is explicitly specified.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"default_agent": "plan"
}
```
The default agent must be a primary agent (not a subagent). This can be a built-in agent like `"build"` or `"plan"`, or a [custom agent](/docs/agents) you've defined. If the specified agent doesn't exist or is a subagent, OpenCode will fall back to `"build"` with a warning.
This setting applies across all interfaces: TUI, CLI (`opencode run`), desktop app, and GitHub Action.
---
### Sharing
You can configure the [share](/docs/share) feature through the `share` option.
+1
View File
@@ -81,6 +81,7 @@ Or you can set it up manually.
## Configuration
- `model`: The model to use with OpenCode. Takes the format of `provider/model`. This is **required**.
- `agent`: The agent to use. Must be a primary agent. Falls back to `default_agent` from config or `"build"` if not found.
- `share`: Whether to share the OpenCode session. Defaults to **true** for public repositories.
- `prompt`: Optional custom prompt to override the default behavior. Use this to customize how OpenCode processes requests.
- `token`: Optional GitHub access token for performing operations such as creating comments, committing changes, and opening pull requests. By default, OpenCode uses the installation access token from the OpenCode GitHub App, so commits, comments, and pull requests appear as coming from the app.
+3 -1
View File
@@ -60,7 +60,7 @@ OpenCode config.
}
```
Here the full ID is `provider_id/model_id`.
Here the full ID is `provider_id/model_id`. For example, if you're using [OpenCode Zen](/docs/zen), you would use `opencode/gpt-5.1-codex` for GPT 5.1 Codex.
If you've configured a [custom provider](/docs/providers#custom), the `provider_id` is key from the `provider` part of your config, and the `model_id` is the key from `provider.models`.
@@ -117,6 +117,7 @@ You can also define custom models that extend built-in ones and can optionally u
"models": {
"gpt-5-high": {
"id": "gpt-5",
"name": "MyGPT5 (High Reasoning)",
"options": {
"reasoningEffort": "high",
"textVerbosity": "low",
@@ -125,6 +126,7 @@ You can also define custom models that extend built-in ones and can optionally u
},
"gpt-5-low": {
"id": "gpt-5",
"name": "MyGPT5 (Low Reasoning)",
"options": {
"reasoningEffort": "low",
"textVerbosity": "low",
+18
View File
@@ -0,0 +1,18 @@
import type { APIRoute } from "astro"
import { getCollection } from "astro:content"
export const GET: APIRoute = async ({ params }) => {
const slug = params.slug || "index"
const docs = await getCollection("docs")
const doc = docs.find((d) => d.id === slug)
if (!doc) {
return new Response("Not found", { status: 404 })
}
return new Response(doc.body, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
},
})
}
Regular → Executable
+1 -1
View File
@@ -4,7 +4,7 @@ import { Script } from "@opencode-ai/script"
import { $ } from "bun"
if (!Script.preview) {
await $`gh release edit ${process.env.OPENCODE_RELEASE_TAG} --draft=false`
await $`gh release edit v${Script.version} --draft=false`
}
await $`bun install`
+22 -16
View File
@@ -27,6 +27,20 @@ if (!Script.preview) {
const opencode = await createOpencode()
const session = await opencode.client.session.create()
console.log("generating changelog since " + previous)
const team = [
"actions-user",
"opencode",
"rekram1-node",
"thdxr",
"kommander",
"jayair",
"fwang",
"adamdotdevin",
"iamdavidhill",
"opencode-agent[bot]",
]
const raw = await opencode.client.session
.prompt({
path: {
@@ -106,19 +120,6 @@ if (!Script.preview) {
console.log("-----------------------------")
opencode.server.close()
// Get contributors
const team = [
"actions-user",
"opencode",
"rekram1-node",
"thdxr",
"kommander",
"jayair",
"fwang",
"adamdotdevin",
"iamdavidhill",
"opencode-agent[bot]",
]
const compare =
await $`gh api "/repos/sst/opencode/compare/v${previous}...HEAD" --jq '.commits[] | {login: .author.login, message: .commit.message}'`.text()
const contributors = new Map<string, string[]>()
@@ -180,6 +181,8 @@ await import(`../packages/plugin/script/publish.ts`)
const dir = new URL("..", import.meta.url).pathname
process.chdir(dir)
let output = `version=${Script.version}\n`
if (!Script.preview) {
await $`git commit -am "release: v${Script.version}"`
await $`git tag v${Script.version}`
@@ -189,7 +192,10 @@ if (!Script.preview) {
await new Promise((resolve) => setTimeout(resolve, 5_000))
await $`gh release create v${Script.version} -d --title "v${Script.version}" --notes ${notes.join("\n") || "No notable changes"} ./packages/opencode/dist/*.zip ./packages/opencode/dist/*.tar.gz`
const release = await $`gh release view v${Script.version} --json id,tagName`.json()
if (process.env.GITHUB_OUTPUT) {
await Bun.write(process.env.GITHUB_OUTPUT, `releaseId=${release.id}\ntagName=${release.tagName}\n`)
}
output += `release=${release.id}\n`
output += `tag=${release.tagName}\n`
}
if (process.env.GITHUB_OUTPUT) {
await Bun.write(process.env.GITHUB_OUTPUT, output)
}
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.0.170",
"version": "1.0.181",
"publisher": "sst-dev",
"repository": {
"type": "git",