Merge branch 'dev' into fix/provider-empty-models-crash

This commit is contained in:
Adam Hosker 2025-12-21 11:18:59 +00:00 committed by GitHub
commit aa6114ae02
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 1882 additions and 432 deletions

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 }}

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
README.zh-TW.md Normal file
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)

View file

@ -20,7 +20,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.0.176",
"version": "1.0.184",
"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.176",
"version": "1.0.184",
"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.176",
"version": "1.0.184",
"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.176",
"version": "1.0.184",
"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.176",
"version": "1.0.184",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@ -171,7 +171,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.0.176",
"version": "1.0.184",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@ -200,7 +200,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.0.176",
"version": "1.0.184",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@ -216,7 +216,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.0.176",
"version": "1.0.184",
"bin": {
"opencode": "./bin/opencode",
},
@ -308,7 +308,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.0.176",
"version": "1.0.184",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@ -328,7 +328,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.0.176",
"version": "1.0.184",
"devDependencies": {
"@hey-api/openapi-ts": "0.88.1",
"@tsconfig/node22": "catalog:",
@ -339,7 +339,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.0.176",
"version": "1.0.184",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@ -352,7 +352,7 @@
},
"packages/tauri": {
"name": "@opencode-ai/tauri",
"version": "1.0.176",
"version": "1.0.184",
"dependencies": {
"@opencode-ai/desktop": "workspace:*",
"@solid-primitives/storage": "catalog:",
@ -379,7 +379,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.0.176",
"version": "1.0.184",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@ -414,7 +414,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.0.176",
"version": "1.0.184",
"dependencies": {
"zod": "catalog:",
},
@ -425,7 +425,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.0.176",
"version": "1.0.184",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",

6
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1766025857,
"narHash": "sha256-Lav5jJazCW4mdg1iHcROpuXqmM94BWJvabLFWaJVJp0=",
"lastModified": 1766125104,
"narHash": "sha256-l/YGrEpLromL4viUo5GmFH3K5M1j0Mb9O+LiaeCPWEM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "def3da69945bbe338c373fddad5a1bb49cf199ce",
"rev": "7d853e518814cca2a657b72eeba67ae20ebf7059",
"type": "github"
},
"original": {

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 }}

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",

View file

@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.0.176",
"version": "1.0.184",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",

View file

@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.0.176",
"version": "1.0.184",
"private": true,
"type": "module",
"dependencies": {

View file

@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.0.176",
"version": "1.0.184",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",

View file

@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.0.176",
"version": "1.0.184",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",

View file

@ -1,6 +1,6 @@
{
"name": "@opencode-ai/desktop",
"version": "1.0.176",
"version": "1.0.184",
"description": "",
"type": "module",
"exports": {

View file

@ -10,6 +10,7 @@ import { Select } from "@opencode-ai/ui/select"
import { TextField } from "@opencode-ai/ui/text-field"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { base64Decode } from "@opencode-ai/util/encode"
import { useCommand } from "@/context/command"
import { getFilename } from "@opencode-ai/util/path"
import { A, useParams } from "@solidjs/router"
import { createMemo, createResource, Show } from "solid-js"
@ -24,6 +25,7 @@ export function Header(props: {
const globalSDK = useGlobalSDK()
const layout = useLayout()
const params = useParams()
const command = useCommand()
return (
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
@ -80,9 +82,18 @@ export function Header(props: {
/>
</div>
<Show when={currentSession()}>
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
New session
</Button>
<Tooltip
value={
<div class="flex items-center gap-2">
<span>New session</span>
<span class="text-icon-base text-12-medium">{command.keybind("session.new")}</span>
</div>
}
>
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
New session
</Button>
</Tooltip>
</Show>
</div>
<div class="flex items-center gap-4">
@ -91,7 +102,7 @@ export function Header(props: {
value={
<div class="flex items-center gap-2">
<span>Toggle terminal</span>
<span class="text-icon-base text-12-medium">Ctrl `</span>
<span class="text-icon-base text-12-medium">{command.keybind("terminal.toggle")}</span>
</div>
}
>

View file

@ -19,7 +19,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
import { DialogSelectModel } from "@/components/dialog-select-model"
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
import { useProviders } from "@/hooks/use-providers"
import { useCommand, formatKeybind } from "@/context/command"
import { useCommand } from "@/context/command"
import { persisted } from "@/utils/persist"
import { Identifier } from "@/utils/id"
@ -889,8 +889,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
custom
</span>
</Show>
<Show when={cmd.keybind}>
<span class="text-12-regular text-text-subtle">{formatKeybind(cmd.keybind!)}</span>
<Show when={command.keybind(cmd.id)}>
<span class="text-12-regular text-text-subtle">{command.keybind(cmd.id)}</span>
</Show>
</div>
</button>
@ -990,26 +990,46 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</div>
</Match>
<Match when={store.mode === "normal"}>
<Select
options={local.agent.list().map((agent) => agent.name)}
current={local.agent.current().name}
onSelect={local.agent.set}
class="capitalize"
variant="ghost"
/>
<Button
as="div"
variant="ghost"
onClick={() =>
dialog.show(() =>
providers.paid().length > 0 ? <DialogSelectModel /> : <DialogSelectModelUnpaid />,
)
<Tooltip
placement="top"
value={
<div class="flex items-center gap-2">
<span>Cycle agent</span>
<span class="text-icon-base text-12-medium">{command.keybind("agent.cycle")}</span>
</div>
}
>
{local.model.current()?.name ?? "Select model"}
<span class="ml-0.5 text-text-weak text-12-regular">{local.model.current()?.provider.name}</span>
<Icon name="chevron-down" size="small" />
</Button>
<Select
options={local.agent.list().map((agent) => agent.name)}
current={local.agent.current().name}
onSelect={local.agent.set}
class="capitalize"
variant="ghost"
/>
</Tooltip>
<Tooltip
placement="top"
value={
<div class="flex items-center gap-2">
<span>Choose model</span>
<span class="text-icon-base text-12-medium">{command.keybind("model.choose")}</span>
</div>
}
>
<Button
as="div"
variant="ghost"
onClick={() =>
dialog.show(() =>
providers.paid().length > 0 ? <DialogSelectModel /> : <DialogSelectModelUnpaid />,
)
}
>
{local.model.current()?.name ?? "Select model"}
<span class="ml-0.5 text-text-weak text-12-regular">{local.model.current()?.provider.name}</span>
<Icon name="chevron-down" size="small" />
</Button>
</Tooltip>
</Match>
</Switch>
</div>

View file

@ -226,6 +226,11 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
}
}
},
keybind(id: string) {
const option = options().find((x) => x.id === id || x.id === "suggested." + id)
if (!option?.keybind) return ""
return formatKeybind(option.keybind)
},
show: showPalette,
keybinds(enabled: boolean) {
setSuspendCount((count) => count + (enabled ? -1 : 1))

View file

@ -46,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>,
}),
@ -156,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) {
@ -186,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)) {

View file

@ -360,7 +360,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const init = async (path: string) => {
const relativePath = relative(path)
if (!store.node[relativePath]) await fetch(path)
if (store.node[relativePath].loaded) return
if (store.node[relativePath]?.loaded) return
return load(relativePath)
}
@ -380,7 +380,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
context.addActive()
if (options?.pinned) setStore("node", path, "pinned", true)
if (options?.view && store.node[relativePath].view === undefined) setStore("node", path, "view", options.view)
if (store.node[relativePath].loaded) return
if (store.node[relativePath]?.loaded) return
return load(relativePath)
}

View file

@ -62,12 +62,32 @@ function formatInitError(error: InitError): string {
}
}
function formatError(error: unknown): string {
function formatErrorChain(error: unknown, depth = 0): string {
if (!error) return "Unknown error"
if (isInitError(error)) return formatInitError(error)
if (error instanceof Error) return `${error.name}: ${error.message}\n\n${error.stack}`
if (typeof error === "string") return error
return JSON.stringify(error, null, 2)
const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
if (isInitError(error)) {
return indent + formatInitError(error)
}
if (error instanceof Error) {
const parts = [indent + `${error.name}: ${error.message}`]
if (error.stack) {
parts.push(error.stack)
}
if (error.cause) {
parts.push(formatErrorChain(error.cause, depth + 1))
}
return parts.join("\n\n")
}
if (typeof error === "string") return indent + error
return indent + JSON.stringify(error, null, 2)
}
function formatError(error: unknown): string {
return formatErrorChain(error, 0)
}
interface ErrorPageProps {

View file

@ -674,7 +674,17 @@ export default function Layout(props: ParentProps) {
/>
</Show>
<div class="flex flex-col items-start self-stretch gap-4 p-2 min-h-0 overflow-hidden">
<Tooltip class="shrink-0" placement="right" value="Toggle sidebar" inactive={layout.sidebar.opened()}>
<Tooltip
class="shrink-0"
placement="right"
value={
<div class="flex items-center gap-2">
<span>Toggle sidebar</span>
<span class="text-icon-base text-12-medium">{command.keybind("sidebar.toggle")}</span>
</div>
}
inactive={layout.sidebar.opened()}
>
<Button
variant="ghost"
size="large"
@ -762,7 +772,16 @@ export default function Layout(props: ParentProps) {
</Match>
</Switch>
<Show when={platform.openDirectoryPickerDialog}>
<Tooltip placement="right" value="Open project" inactive={layout.sidebar.opened()}>
<Tooltip
placement="right"
value={
<div class="flex items-center gap-2">
<span>Open project</span>
<span class="text-icon-base text-12-medium">{command.keybind("project.open")}</span>
</div>
}
inactive={layout.sidebar.opened()}
>
<Button
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
variant="ghost"

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,226 @@ 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="relative bg-background-base size-full overflow-hidden flex flex-col">
<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={
<div class="flex items-center gap-2">
<span>Open file</span>
<span class="text-icon-base text-12-medium">{command.keybind("file.open")}</span>
</div>
}
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>
@ -849,7 +782,15 @@ export default function Page() {
<For each={terminal.all()}>{(pty) => <SortableTerminalTab terminal={pty} />}</For>
</SortableProvider>
<div class="h-full flex items-center justify-center">
<Tooltip value="New Terminal" class="flex items-center">
<Tooltip
value={
<div class="flex items-center gap-2">
<span>New terminal</span>
<span class="text-icon-base text-12-medium">{command.keybind("terminal.new")}</span>
</div>
}
class="flex items-center"
>
<IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={terminal.new} />
</Tooltip>
</div>

View file

@ -10,5 +10,6 @@ export default defineConfig({
},
build: {
target: "esnext",
sourcemap: true,
},
})

View file

@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.0.176",
"version": "1.0.184",
"private": true,
"type": "module",
"scripts": {

View file

@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.0.176"
version = "1.0.184"
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.176/opencode-darwin-arm64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.184/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.176/opencode-darwin-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.184/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.176/opencode-linux-arm64.tar.gz"
archive = "https://github.com/sst/opencode/releases/download/v1.0.184/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.176/opencode-linux-x64.tar.gz"
archive = "https://github.com/sst/opencode/releases/download/v1.0.184/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.176/opencode-windows-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.184/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]

View file

@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.0.176",
"version": "1.0.184",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",

View file

@ -1,13 +1,13 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.0.176",
"version": "1.0.184",
"name": "opencode",
"type": "module",
"private": true,
"scripts": {
"typecheck": "tsgo --noEmit",
"test": "bun test",
"build": "./script/build.ts",
"build": "bun run script/build.ts",
"dev": "bun run --conditions=browser ./src/index.ts",
"random": "echo 'Random script updated at $(date)' && echo 'Change queued successfully' && echo 'Another change made' && echo 'Yet another change' && echo 'One more change' && echo 'Final change' && echo 'Another final change' && echo 'Yet another final change'",
"clean": "echo 'Cleaning up...' && rm -rf node_modules dist",

View file

@ -16,6 +16,7 @@ import pkg from "../package.json"
import { Script } from "@opencode-ai/script"
const singleFlag = process.argv.includes("--single")
const baselineFlag = process.argv.includes("--baseline")
const skipInstall = process.argv.includes("--skip-install")
const allTargets: {
@ -78,7 +79,19 @@ const allTargets: {
]
const targets = singleFlag
? allTargets.filter((item) => item.os === process.platform && item.arch === process.arch)
? allTargets.filter((item) => {
if (item.os !== process.platform || item.arch !== process.arch) {
return false
}
// When building for the current platform, prefer a single native binary by default.
// Baseline binaries require additional Bun artifacts and can be flaky to download.
if (item.avx2 === false) {
return baselineFlag
}
return true
})
: allTargets
await $`rm -rf dist`

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"
@ -706,7 +707,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) {
@ -808,7 +810,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 }

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,27 @@ 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
}
}
const hasPrimaryAgents = Object.values(result).filter((a) => a.mode !== "subagent" && !a.hidden).length > 0
if (!hasPrimaryAgents) {
throw new Config.InvalidError({
path: "config",
message: "No primary agents are available. Please configure at least one agent with mode 'primary' or 'all'.",
})
}
return result
})
@ -256,6 +281,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())

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"),

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 }],
})

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

View file

@ -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(() => [

View file

@ -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!)),

View file

@ -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(() => {

View file

@ -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"
}
}
}

View file

@ -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"
}
}
}

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")

View file

@ -666,6 +666,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()

View file

@ -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
}

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({

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({

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,

View file

@ -0,0 +1,146 @@
import { test, expect } from "bun:test"
import path from "path"
import fs from "fs/promises"
import { tmpdir } from "../fixture/fixture"
import { Instance } from "../../src/project/instance"
import { Agent } from "../../src/agent/agent"
test("loads built-in agents when no custom agents configured", async () => {
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const agents = await Agent.list()
const names = agents.map((a) => a.name)
expect(names).toContain("build")
expect(names).toContain("plan")
},
})
})
test("custom subagent works alongside built-in primary agents", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
const opencodeDir = path.join(dir, ".opencode")
await fs.mkdir(opencodeDir, { recursive: true })
const agentDir = path.join(opencodeDir, "agent")
await fs.mkdir(agentDir, { recursive: true })
await Bun.write(
path.join(agentDir, "helper.md"),
`---
model: test/model
mode: subagent
---
Helper subagent prompt`,
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const agents = await Agent.list()
const helper = agents.find((a) => a.name === "helper")
expect(helper).toBeDefined()
expect(helper?.mode).toBe("subagent")
// Built-in primary agents should still exist
const build = agents.find((a) => a.name === "build")
expect(build).toBeDefined()
expect(build?.mode).toBe("primary")
},
})
})
test("throws error when all primary agents are disabled", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
agent: {
build: { disable: true },
plan: { disable: true },
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
try {
await Agent.list()
expect(true).toBe(false) // should not reach here
} catch (e: any) {
expect(e.data?.message).toContain("No primary agents are available")
}
},
})
})
test("does not throw when at least one primary agent remains", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
agent: {
build: { disable: true },
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const agents = await Agent.list()
const plan = agents.find((a) => a.name === "plan")
expect(plan).toBeDefined()
expect(plan?.mode).toBe("primary")
},
})
})
test("custom primary agent satisfies requirement when built-ins disabled", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
const opencodeDir = path.join(dir, ".opencode")
await fs.mkdir(opencodeDir, { recursive: true })
const agentDir = path.join(opencodeDir, "agent")
await fs.mkdir(agentDir, { recursive: true })
await Bun.write(
path.join(agentDir, "custom.md"),
`---
model: test/model
mode: primary
---
Custom primary agent`,
)
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
agent: {
build: { disable: true },
plan: { disable: true },
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const agents = await Agent.list()
const custom = agents.find((a) => a.name === "custom")
expect(custom).toBeDefined()
expect(custom?.mode).toBe("primary")
},
})
})

View file

@ -450,6 +450,38 @@ test("merges plugin arrays from global and local configs", async () => {
})
})
test("does not error when only custom agent is a subagent", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
const opencodeDir = path.join(dir, ".opencode")
await fs.mkdir(opencodeDir, { recursive: true })
const agentDir = path.join(opencodeDir, "agent")
await fs.mkdir(agentDir, { recursive: true })
await Bun.write(
path.join(agentDir, "helper.md"),
`---
model: test/model
mode: subagent
---
Helper subagent prompt`,
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const config = await Config.get()
expect(config.agent?.["helper"]).toEqual({
name: "helper",
model: "test/model",
mode: "subagent",
prompt: "Helper subagent prompt",
})
},
})
})
test("deduplicates duplicate plugins from global and local configs", async () => {
await using tmp = await tmpdir({
init: async (dir) => {

View file

@ -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 = [

View file

@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.0.176",
"version": "1.0.184",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",

View file

@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.0.176",
"version": "1.0.184",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",

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 })

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

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"
},

View file

@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.0.176",
"version": "1.0.184",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",

View file

@ -14,7 +14,7 @@
<meta property="og:image" content="/social-share.png" />
<meta property="twitter:image" content="/social-share.png" />
</head>
<body class="antialiased overscroll-none select-none text-12-regular overflow-hidden">
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
<script>
;(function () {
const savedTheme = localStorage.getItem("theme") || "oc-1"

View file

@ -1,7 +1,7 @@
{
"name": "@opencode-ai/tauri",
"private": true,
"version": "1.0.176",
"version": "1.0.184",
"type": "module",
"scripts": {
"typecheck": "tsgo -b",

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}`

View file

@ -6,7 +6,7 @@ use std::{
sync::{Arc, Mutex},
time::{Duration, Instant},
};
use tauri::{AppHandle, LogicalSize, Manager, RunEvent, WebviewUrl, WebviewWindow};
use tauri::{AppHandle, LogicalSize, Manager, RunEvent, WebviewUrl, WebviewWindow, path::BaseDirectory};
use tauri_plugin_clipboard_manager::ClipboardExt;
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
@ -97,6 +97,11 @@ fn spawn_sidecar(app: &AppHandle, port: u32) -> CommandChild {
let log_state = app.state::<LogState>();
let log_state_clone = log_state.inner().clone();
let state_dir = app
.path()
.resolve("", BaseDirectory::AppLocalData)
.expect("Failed to resolve app local data dir");
#[cfg(target_os = "windows")]
let (mut rx, child) = app
.shell()
@ -104,6 +109,7 @@ fn spawn_sidecar(app: &AppHandle, port: u32) -> CommandChild {
.unwrap()
.env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
.env("OPENCODE_CLIENT", "desktop")
.env("XDG_STATE_HOME", &state_dir)
.args(["serve", &format!("--port={port}")])
.spawn()
.expect("Failed to spawn opencode");
@ -120,6 +126,7 @@ fn spawn_sidecar(app: &AppHandle, port: u32) -> CommandChild {
.command(&shell)
.env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
.env("OPENCODE_CLIENT", "desktop")
.env("XDG_STATE_HOME", &state_dir)
.args([
"-il",
"-c",

View file

@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.0.176",
"version": "1.0.184",
"type": "module",
"exports": {
"./*": "./src/components/*.tsx",

View file

@ -603,7 +603,9 @@ ToolRegistry.register({
icon="checklist"
trigger={{
title: "To-dos",
subtitle: `${props.input.todos?.filter((t: any) => t.status === "completed").length}/${props.input.todos?.length}`,
subtitle: props.input.todos
? `${props.input.todos.filter((t: any) => t.status === "completed").length}/${props.input.todos.length}`
: "",
}}
>
<Show when={props.input.todos?.length}>

View file

@ -1,6 +1,6 @@
{
"name": "@opencode-ai/util",
"version": "1.0.176",
"version": "1.0.184",
"private": true,
"type": "module",
"exports": {

View file

@ -1,9 +1,13 @@
export function base64Encode(value: string) {
return btoa(value).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
const bytes = new TextEncoder().encode(value)
const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("")
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
}
export function base64Decode(value: string) {
return atob(value.replace(/-/g, "+").replace(/_/g, "/"))
const binary = atob(value.replace(/-/g, "+").replace(/_/g, "/"))
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0))
return new TextDecoder().decode(bytes)
}
export async function hash(content: string, algorithm = "SHA-256"): Promise<string> {

View file

@ -1,7 +1,7 @@
{
"name": "@opencode-ai/web",
"type": "module",
"version": "1.0.176",
"version": "1.0.184",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",

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

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.

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.

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",

View file

@ -77,7 +77,6 @@ You can also access our models through the following API endpoints.
| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| MiniMax M2.1 | minimax-m2.1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Gemini 3 Pro | gemini-3-pro | `https://opencode.ai/zen/v1/models/gemini-3-pro` | `@ai-sdk/google` |
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
| GLM 4.6 | glm-4.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@ -110,7 +109,6 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
| Model | Input | Output | Cached Read | Cached Write |
| --------------------------------- | ------ | ------ | ----------- | ------------ |
| Big Pickle | Free | Free | Free | - |
| MiniMax M2.1 | Free | Free | Free | - |
| Grok Code Fast 1 | Free | Free | Free | - |
| GLM 4.6 | $0.60 | $2.20 | $0.10 | - |
| Kimi K2 | $0.40 | $2.50 | - | - |
@ -144,7 +142,6 @@ Credit card fees are passed along at cost; we don't charge anything beyond that.
The free models:
- Grok Code Fast 1 is currently free on OpenCode for a limited time. The xAI team is using this time to collect feedback and improve Grok Code.
- MiniMax M2.1 is currently free on OpenCode for a limited time. The MiniMax team is using this time to collect feedback and improve M2.1.
- Big Pickle is a stealth model that's free on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
<a href={email}>Contact us</a> if you have any questions.
@ -156,7 +153,6 @@ The free models:
All our models are hosted in the US. Our providers follow a zero-retention policy and do not use your data for model training, with the following exceptions:
- Grok Code Fast 1: During its free period, collected data may be used to improve Grok Code.
- MiniMax M2.1: During its free period, collected data may be used to improve M2.1.
- Big Pickle: During its free period, collected data may be used to improve the model.
- OpenAI APIs: Requests are retained for 30 days in accordance with [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
- Anthropic APIs: Requests are retained for 30 days in accordance with [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage).

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",
},
})
}

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`

View file

@ -181,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}`
@ -190,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)
}

View file

@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.0.176",
"version": "1.0.184",
"publisher": "sst-dev",
"repository": {
"type": "git",