Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f421bac0e5 | ||
|
|
5b2797295f | ||
|
|
cfaac9f2e1 | ||
|
|
0b046d6cf0 | ||
|
|
3d822e5f79 | ||
|
|
f9cef22a53 | ||
|
|
b5d7d3dec1 | ||
|
|
182630e0d7 | ||
|
|
c81506b28d | ||
|
|
6c40bfe043 | ||
|
|
9caaae6a18 | ||
|
|
ad6a5e6157 | ||
|
|
7dd8ea58c2 | ||
|
|
3b261e0125 | ||
|
|
426791f68a | ||
|
|
c7cade2494 |
+115
@@ -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>
|
||||||
|
|
||||||
|
[](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)
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "2.0.0",
|
"@ai-sdk/anthropic": "2.0.0",
|
||||||
"@ai-sdk/openai": "2.0.2",
|
"@ai-sdk/openai": "2.0.2",
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -171,7 +171,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
@@ -200,7 +200,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -216,7 +216,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -308,7 +308,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
@@ -328,7 +328,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hey-api/openapi-ts": "0.88.1",
|
"@hey-api/openapi-ts": "0.88.1",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
@@ -339,7 +339,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -352,7 +352,7 @@
|
|||||||
},
|
},
|
||||||
"packages/tauri": {
|
"packages/tauri": {
|
||||||
"name": "@opencode-ai/tauri",
|
"name": "@opencode-ai/tauri",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/desktop": "workspace:*",
|
"@opencode-ai/desktop": "workspace:*",
|
||||||
"@solid-primitives/storage": "catalog:",
|
"@solid-primitives/storage": "catalog:",
|
||||||
@@ -379,7 +379,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -414,7 +414,7 @@
|
|||||||
},
|
},
|
||||||
"packages/util": {
|
"packages/util": {
|
||||||
"name": "@opencode-ai/util",
|
"name": "@opencode-ai/util",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -425,7 +425,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
opened: false,
|
opened: false,
|
||||||
height: 280,
|
height: 280,
|
||||||
},
|
},
|
||||||
review: {
|
session: {
|
||||||
state: "pane" as "pane" | "tab",
|
width: 600,
|
||||||
},
|
},
|
||||||
sessionTabs: {} as Record<string, SessionTabs>,
|
sessionTabs: {} as Record<string, SessionTabs>,
|
||||||
}),
|
}),
|
||||||
@@ -156,13 +156,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
setStore("terminal", "height", height)
|
setStore("terminal", "height", height)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
review: {
|
session: {
|
||||||
state: createMemo(() => store.review?.state ?? "closed"),
|
width: createMemo(() => store.session?.width ?? 600),
|
||||||
pane() {
|
resize(width: number) {
|
||||||
setStore("review", "state", "pane")
|
if (!store.session) {
|
||||||
},
|
setStore("session", { width })
|
||||||
tab() {
|
} else {
|
||||||
setStore("review", "state", "tab")
|
setStore("session", "width", width)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tabs(sessionKey: string) {
|
tabs(sessionKey: string) {
|
||||||
@@ -186,14 +187,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async open(tab: string) {
|
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: [] }
|
const current = store.sessionTabs[sessionKey] ?? { all: [] }
|
||||||
if (tab !== "review") {
|
if (tab !== "review") {
|
||||||
if (!current.all.includes(tab)) {
|
if (!current.all.includes(tab)) {
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
|
|||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
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 { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { useCodeComponent } from "@opencode-ai/ui/context/code"
|
import { useCodeComponent } from "@opencode-ai/ui/context/code"
|
||||||
@@ -50,7 +49,7 @@ import { DialogSelectFile } from "@/components/dialog-select-file"
|
|||||||
import { DialogSelectModel } from "@/components/dialog-select-model"
|
import { DialogSelectModel } from "@/components/dialog-select-model"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { useNavigate, useParams } from "@solidjs/router"
|
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 { useSDK } from "@/context/sdk"
|
||||||
import { usePrompt } from "@/context/prompt"
|
import { usePrompt } from "@/context/prompt"
|
||||||
import { extractPromptFromParts } from "@/utils/prompt"
|
import { extractPromptFromParts } from "@/utils/prompt"
|
||||||
@@ -118,27 +117,8 @@ export default function Page() {
|
|||||||
setActiveMessage(msgs[targetIndex])
|
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 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({
|
const [store, setStore] = createStore({
|
||||||
clickTimer: undefined as number | undefined,
|
clickTimer: undefined as number | undefined,
|
||||||
activeDraggable: undefined as string | 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 (
|
return (
|
||||||
<div class="relative bg-background-base size-full overflow-x-hidden flex flex-col">
|
<div class="relative bg-background-base size-full overflow-x-hidden flex flex-col">
|
||||||
<div class="min-h-0 grow w-full">
|
<div class="min-h-0 grow w-full flex">
|
||||||
<DragDropProvider
|
{/* Session pane - always visible */}
|
||||||
onDragStart={handleDragStart}
|
<div
|
||||||
onDragEnd={handleDragEnd}
|
class="@container relative shrink-0 py-3 flex flex-col gap-6 min-h-0 h-full bg-background-stronger"
|
||||||
onDragOver={handleDragOver}
|
style={{ width: showTabs() ? `${layout.session.width()}px` : "100%" }}
|
||||||
collisionDetector={closestCenter}
|
|
||||||
>
|
>
|
||||||
<DragDropSensors />
|
<div class="flex-1 min-h-0 overflow-hidden">
|
||||||
<ConstrainDragYAxis />
|
<Switch>
|
||||||
<Tabs value={tabs().active() ?? "chat"} onChange={tabs().open}>
|
<Match when={params.id}>
|
||||||
<div class="sticky top-0 shrink-0 flex">
|
<div class="flex items-start justify-start h-full min-h-0">
|
||||||
<Tabs.List>
|
<SessionMessageRail
|
||||||
<Tabs.Trigger value="chat">
|
messages={visibleUserMessages()}
|
||||||
<div class="flex gap-x-[17px] items-center">
|
current={activeMessage()}
|
||||||
<div>Session</div>
|
onMessageSelect={setActiveMessage}
|
||||||
<Tooltip
|
wide={!showTabs()}
|
||||||
value={`${new Intl.NumberFormat("en-US", {
|
/>
|
||||||
notation: "compact",
|
<Show when={activeMessage()}>
|
||||||
compactDisplay: "short",
|
<SessionTurn
|
||||||
}).format(tokens() ?? 0)} Tokens`}
|
sessionID={params.id!}
|
||||||
class="flex items-center gap-1.5"
|
messageID={activeMessage()!.id}
|
||||||
>
|
stepsExpanded={store.stepsExpanded}
|
||||||
<ProgressCircle percentage={context() ?? 0} />
|
onStepsExpandedToggle={() => setStore("stepsExpanded", (x) => !x)}
|
||||||
<div class="text-14-regular text-text-weak text-left w-7">{context() ?? 0}%</div>
|
onUserInteracted={() => setStore("userInteracted", true)}
|
||||||
</Tooltip>
|
classes={{
|
||||||
</div>
|
root: "pb-20 flex-1 min-w-0",
|
||||||
</Tabs.Trigger>
|
content: "pb-20",
|
||||||
<Show when={layout.review.state() === "tab" && diffs().length}>
|
container:
|
||||||
<Tabs.Trigger
|
"w-full " +
|
||||||
value="review"
|
(!showTabs()
|
||||||
closeButton={
|
? "max-w-200 mx-auto px-6"
|
||||||
<Tooltip value="Close tab" placement="bottom">
|
: visibleUserMessages().length > 1
|
||||||
<IconButton icon="collapse" size="normal" variant="ghost" onClick={layout.review.pane} />
|
? "pr-6 pl-18"
|
||||||
</Tooltip>
|
: "px-6"),
|
||||||
}
|
}}
|
||||||
>
|
|
||||||
<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 />)}
|
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.List>
|
</Match>
|
||||||
</div>
|
<Match when={true}>
|
||||||
<Tabs.Content
|
<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">
|
||||||
value="chat"
|
<div class="text-20-medium text-text-weaker">New session</div>
|
||||||
class="@container select-text flex flex-col flex-1 min-h-0 overflow-y-hidden contain-strict"
|
<div class="flex justify-center items-center gap-3">
|
||||||
>
|
<Icon name="folder" size="small" />
|
||||||
<div
|
<div class="text-12-medium text-text-weak">
|
||||||
classList={{
|
{getDirectory(sync.data.path.directory)}
|
||||||
"w-full flex-1 min-h-0": true,
|
<span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
|
||||||
grid: layout.review.state() === "tab",
|
</div>
|
||||||
flex: layout.review.state() === "pane",
|
</div>
|
||||||
}}
|
<Show when={sync.project}>
|
||||||
>
|
{(project) => (
|
||||||
<div
|
<div class="flex justify-center items-center gap-3">
|
||||||
classList={{
|
<Icon name="pencil-line" size="small" />
|
||||||
"relative shrink-0 py-3 flex flex-col gap-6 flex-1 min-h-0 w-full": true,
|
<div class="text-12-medium text-text-weak">
|
||||||
"max-w-146 mx-auto": !wide(),
|
Last modified
|
||||||
}}
|
<span class="text-text-strong">
|
||||||
>
|
{DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()}
|
||||||
<Switch>
|
</span>
|
||||||
<Match when={params.id}>
|
</div>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
)}
|
||||||
<Match when={true}>
|
</Show>
|
||||||
<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>
|
||||||
<div class="text-20-medium text-text-weaker">New session</div>
|
</Match>
|
||||||
<div class="flex justify-center items-center gap-3">
|
</Switch>
|
||||||
<Icon name="folder" size="small" />
|
</div>
|
||||||
<div class="text-12-medium text-text-weak">
|
<div class="absolute inset-x-0 bottom-8 flex flex-col justify-center items-center z-50">
|
||||||
{getDirectory(sync.data.path.directory)}
|
<div
|
||||||
<span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
|
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>
|
||||||
</div>
|
</div>
|
||||||
<Show when={sync.project}>
|
</Tabs.Trigger>
|
||||||
{(project) => (
|
</Show>
|
||||||
<div class="flex justify-center items-center gap-3">
|
<SortableProvider ids={tabs().all() ?? []}>
|
||||||
<Icon name="pencil-line" size="small" />
|
<For each={tabs().all() ?? []}>
|
||||||
<div class="text-12-medium text-text-weak">
|
{(tab) => <SortableTab tab={tab} onTabClick={handleTabClick} onTabClose={tabs().close} />}
|
||||||
Last modified
|
</For>
|
||||||
<span class="text-text-strong">
|
</SortableProvider>
|
||||||
{DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()}
|
<div class="bg-background-base h-full flex items-center justify-center border-b border-border-weak-base px-3">
|
||||||
</span>
|
<Tooltip value="Open file" class="flex items-center">
|
||||||
</div>
|
<IconButton
|
||||||
</div>
|
icon="plus-small"
|
||||||
)}
|
variant="ghost"
|
||||||
</Show>
|
iconSize="large"
|
||||||
</div>
|
onClick={() => dialog.show(() => <DialogSelectFile />)}
|
||||||
</Match>
|
/>
|
||||||
</Switch>
|
</Tooltip>
|
||||||
<div class="absolute inset-x-0 bottom-8 flex flex-col justify-center items-center z-50">
|
</div>
|
||||||
<div class="w-full max-w-200 px-6">
|
</Tabs.List>
|
||||||
<PromptInput
|
</div>
|
||||||
ref={(el) => {
|
<Show when={diffs().length}>
|
||||||
inputRef = el
|
<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>
|
|
||||||
</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>
|
</Tabs.Content>
|
||||||
)
|
</Show>
|
||||||
}}
|
<For each={tabs().all()}>
|
||||||
</For>
|
{(tab) => {
|
||||||
</Tabs>
|
const [file] = createResource(
|
||||||
<DragOverlay>
|
() => tab,
|
||||||
<Show when={store.activeDraggable}>
|
async (tab) => {
|
||||||
{(draggedFile) => {
|
if (tab.startsWith("file://")) {
|
||||||
const [file] = createResource(
|
return local.file.node(tab.replace("file://", ""))
|
||||||
() => draggedFile(),
|
}
|
||||||
async (tab) => {
|
return undefined
|
||||||
if (tab.startsWith("file://")) {
|
},
|
||||||
return local.file.node(tab.replace("file://", ""))
|
)
|
||||||
}
|
return (
|
||||||
return undefined
|
<Tabs.Content value={tab} class="select-text mt-3">
|
||||||
},
|
<Switch>
|
||||||
)
|
<Match when={file()}>
|
||||||
return (
|
{(f) => (
|
||||||
<div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent">
|
<Dynamic
|
||||||
<Show when={file()}>{(f) => <FileVisual active file={f()} />}</Show>
|
component={codeComponent}
|
||||||
</div>
|
file={{
|
||||||
)
|
name: f().path,
|
||||||
}}
|
contents: f().content?.content ?? "",
|
||||||
</Show>
|
cacheKey: checksum(f().content?.content ?? ""),
|
||||||
</DragOverlay>
|
}}
|
||||||
</DragDropProvider>
|
overflow="scroll"
|
||||||
<Show when={tabs().active()}>
|
class="pb-40"
|
||||||
<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) => {
|
</Match>
|
||||||
inputRef = el
|
</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>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
id = "opencode"
|
id = "opencode"
|
||||||
name = "OpenCode"
|
name = "OpenCode"
|
||||||
description = "The open source coding agent."
|
description = "The open source coding agent."
|
||||||
version = "1.0.180"
|
version = "1.0.182"
|
||||||
schema_version = 1
|
schema_version = 1
|
||||||
authors = ["Anomaly"]
|
authors = ["Anomaly"]
|
||||||
repository = "https://github.com/sst/opencode"
|
repository = "https://github.com/sst/opencode"
|
||||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
|||||||
icon = "./icons/opencode.svg"
|
icon = "./icons/opencode.svg"
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-aarch64]
|
[agent_servers.opencode.targets.darwin-aarch64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.180/opencode-darwin-arm64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.182/opencode-darwin-arm64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-x86_64]
|
[agent_servers.opencode.targets.darwin-x86_64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.180/opencode-darwin-x64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.182/opencode-darwin-x64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-aarch64]
|
[agent_servers.opencode.targets.linux-aarch64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.180/opencode-linux-arm64.tar.gz"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.182/opencode-linux-arm64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-x86_64]
|
[agent_servers.opencode.targets.linux-x86_64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.180/opencode-linux-x64.tar.gz"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.182/opencode-linux-x64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.windows-x86_64]
|
[agent_servers.opencode.targets.windows-x86_64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.180/opencode-windows-x64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.182/opencode-windows-x64.zip"
|
||||||
cmd = "./opencode.exe"
|
cmd = "./opencode.exe"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -262,6 +262,14 @@ export namespace Agent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { createSimpleContext } from "./helper"
|
|||||||
import aura from "./theme/aura.json" with { type: "json" }
|
import aura from "./theme/aura.json" with { type: "json" }
|
||||||
import ayu from "./theme/ayu.json" with { type: "json" }
|
import ayu from "./theme/ayu.json" with { type: "json" }
|
||||||
import catppuccin from "./theme/catppuccin.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 catppuccinMacchiato from "./theme/catppuccin-macchiato.json" with { type: "json" }
|
||||||
import cobalt2 from "./theme/cobalt2.json" with { type: "json" }
|
import cobalt2 from "./theme/cobalt2.json" with { type: "json" }
|
||||||
import cursor from "./theme/cursor.json" with { type: "json" }
|
import cursor from "./theme/cursor.json" with { type: "json" }
|
||||||
@@ -137,6 +138,7 @@ export const DEFAULT_THEMES: Record<string, ThemeJson> = {
|
|||||||
aura,
|
aura,
|
||||||
ayu,
|
ayu,
|
||||||
catppuccin,
|
catppuccin,
|
||||||
|
["catppuccin-frappe"]: catppuccinFrappe,
|
||||||
["catppuccin-macchiato"]: catppuccinMacchiato,
|
["catppuccin-macchiato"]: catppuccinMacchiato,
|
||||||
cobalt2,
|
cobalt2,
|
||||||
cursor,
|
cursor,
|
||||||
@@ -281,14 +283,23 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||||||
ready: false,
|
ready: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(async () => {
|
createEffect(() => {
|
||||||
const custom = await getCustomThemes()
|
getCustomThemes()
|
||||||
setStore(
|
.then((custom) => {
|
||||||
produce((draft) => {
|
setStore(
|
||||||
Object.assign(draft.themes, custom)
|
produce((draft) => {
|
||||||
draft.ready = true
|
Object.assign(draft.themes, custom)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setStore("active", "opencode")
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (store.active !== "system") {
|
||||||
|
setStore("ready", true)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const renderer = useRenderer()
|
const renderer = useRenderer()
|
||||||
@@ -297,8 +308,25 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||||||
size: 16,
|
size: 16,
|
||||||
})
|
})
|
||||||
.then((colors) => {
|
.then((colors) => {
|
||||||
if (!colors.palette[0]) return
|
if (!colors.palette[0]) {
|
||||||
setStore("themes", "system", generateSystem(colors, store.mode))
|
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(() => {
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,7 +32,8 @@ export function FormatError(input: unknown) {
|
|||||||
}
|
}
|
||||||
if (Config.InvalidError.isInstance(input))
|
if (Config.InvalidError.isInstance(input))
|
||||||
return [
|
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(".")) ?? []),
|
...(input.data.issues?.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) ?? []),
|
||||||
].join("\n")
|
].join("\n")
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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 () => {
|
test("deduplicates duplicate plugins from global and local configs", async () => {
|
||||||
await using tmp = await tmpdir({
|
await using tmp = await tmpdir({
|
||||||
init: async (dir) => {
|
init: async (dir) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run src/index.ts",
|
"dev": "bun run src/index.ts",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/tauri",
|
"name": "@opencode-ai/tauri",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo -b",
|
"typecheck": "tsgo -b",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
"./*": "./src/components/*.tsx",
|
"./*": "./src/components/*.tsx",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/util",
|
"name": "@opencode-ai/util",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||||
|
|||||||
@@ -331,6 +331,8 @@ If you don’t 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
|
### Tools
|
||||||
|
|||||||
@@ -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`.
|
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": {
|
"models": {
|
||||||
"gpt-5-high": {
|
"gpt-5-high": {
|
||||||
"id": "gpt-5",
|
"id": "gpt-5",
|
||||||
|
"name": "MyGPT5 (High Reasoning)",
|
||||||
"options": {
|
"options": {
|
||||||
"reasoningEffort": "high",
|
"reasoningEffort": "high",
|
||||||
"textVerbosity": "low",
|
"textVerbosity": "low",
|
||||||
@@ -125,6 +126,7 @@ You can also define custom models that extend built-in ones and can optionally u
|
|||||||
},
|
},
|
||||||
"gpt-5-low": {
|
"gpt-5-low": {
|
||||||
"id": "gpt-5",
|
"id": "gpt-5",
|
||||||
|
"name": "MyGPT5 (Low Reasoning)",
|
||||||
"options": {
|
"options": {
|
||||||
"reasoningEffort": "low",
|
"reasoningEffort": "low",
|
||||||
"textVerbosity": "low",
|
"textVerbosity": "low",
|
||||||
|
|||||||
@@ -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 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.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` |
|
| 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 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` |
|
| 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` |
|
| 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 |
|
| Model | Input | Output | Cached Read | Cached Write |
|
||||||
| --------------------------------- | ------ | ------ | ----------- | ------------ |
|
| --------------------------------- | ------ | ------ | ----------- | ------------ |
|
||||||
| Big Pickle | Free | Free | Free | - |
|
| Big Pickle | Free | Free | Free | - |
|
||||||
| MiniMax M2.1 | Free | Free | Free | - |
|
|
||||||
| Grok Code Fast 1 | Free | Free | Free | - |
|
| Grok Code Fast 1 | Free | Free | Free | - |
|
||||||
| GLM 4.6 | $0.60 | $2.20 | $0.10 | - |
|
| GLM 4.6 | $0.60 | $2.20 | $0.10 | - |
|
||||||
| Kimi K2 | $0.40 | $2.50 | - | - |
|
| 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:
|
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.
|
- 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.
|
- 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.
|
<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:
|
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.
|
- 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.
|
- 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).
|
- 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).
|
- 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).
|
||||||
|
|||||||
@@ -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",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"displayName": "opencode",
|
"displayName": "opencode",
|
||||||
"description": "opencode for VS Code",
|
"description": "opencode for VS Code",
|
||||||
"version": "1.0.180",
|
"version": "1.0.182",
|
||||||
"publisher": "sst-dev",
|
"publisher": "sst-dev",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
Reference in New Issue
Block a user