mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
Merge branch 'dev' into fix/provider-empty-models-crash
This commit is contained in:
commit
aa6114ae02
69 changed files with 1882 additions and 432 deletions
25
.github/workflows/publish.yml
vendored
25
.github/workflows/publish.yml
vendored
|
|
@ -80,8 +80,9 @@ jobs:
|
|||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: false
|
||||
outputs:
|
||||
releaseId: ${{ steps.publish.outputs.releaseId }}
|
||||
tagName: ${{ steps.publish.outputs.tagName }}
|
||||
release: ${{ steps.publish.outputs.release }}
|
||||
tag: ${{ steps.publish.outputs.tag }}
|
||||
version: ${{ steps.publish.outputs.version }}
|
||||
|
||||
publish-tauri:
|
||||
needs: publish
|
||||
|
|
@ -103,7 +104,7 @@ jobs:
|
|||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ needs.publish.outputs.tagName }}
|
||||
ref: ${{ needs.publish.outputs.tag }}
|
||||
|
||||
- uses: apple-actions/import-codesign-certs@v2
|
||||
if: ${{ runner.os == 'macOS' }}
|
||||
|
|
@ -146,20 +147,18 @@ jobs:
|
|||
shared-key: ${{ matrix.settings.target }}
|
||||
|
||||
- name: Prepare
|
||||
if: inputs.bump || inputs.version
|
||||
run: |
|
||||
cd packages/tauri
|
||||
bun ./scripts/prepare.ts
|
||||
env:
|
||||
OPENCODE_BUMP: ${{ inputs.bump }}
|
||||
OPENCODE_VERSION: ${{ inputs.version }}
|
||||
OPENCODE_CHANNEL: latest
|
||||
OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
|
||||
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
RUST_TARGET: ${{ matrix.settings.target }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
OPENCODE_RELEASE_TAG: ${{ needs.publish.outputs.tagName }}
|
||||
|
||||
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
|
||||
- name: Install tauri-cli from portable appimage branch
|
||||
|
|
@ -189,8 +188,8 @@ jobs:
|
|||
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
|
||||
args: --target ${{ matrix.settings.target }} --config src-tauri/tauri.prod.conf.json
|
||||
updaterJsonPreferNsis: true
|
||||
releaseId: ${{ needs.publish.outputs.releaseId }}
|
||||
tagName: ${{ needs.publish.outputs.tagName }}
|
||||
releaseId: ${{ needs.publish.outputs.release }}
|
||||
tagName: ${{ needs.publish.outputs.tag }}
|
||||
releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
|
||||
releaseDraft: true
|
||||
|
||||
|
|
@ -198,13 +197,13 @@ jobs:
|
|||
needs:
|
||||
- publish
|
||||
- publish-tauri
|
||||
if: needs.publish.outputs.tagName
|
||||
if: needs.publish.outputs.tag
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ needs.publish.outputs.tagName }}
|
||||
ref: ${{ needs.publish.outputs.tag }}
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
|
|
@ -221,8 +220,6 @@ jobs:
|
|||
|
||||
- run: ./script/publish-complete.ts
|
||||
env:
|
||||
OPENCODE_BUMP: ${{ inputs.bump }}
|
||||
OPENCODE_VERSION: ${{ inputs.version }}
|
||||
OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
OPENCODE_RELEASE_TAG: ${{ needs.publish.outputs.tagName }}
|
||||
|
|
|
|||
2
.github/workflows/review.yml
vendored
2
.github/workflows/review.yml
vendored
|
|
@ -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
115
README.zh-TW.md
Normal 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>
|
||||
|
||||
[](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)
|
||||
30
bun.lock
30
bun.lock
|
|
@ -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
6
flake.lock
generated
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -318,6 +318,10 @@ function useEnvRunUrl() {
|
|||
return `/${repo.owner}/${repo.repo}/actions/runs/${runId}`
|
||||
}
|
||||
|
||||
function useEnvAgent() {
|
||||
return process.env["AGENT"] || undefined
|
||||
}
|
||||
|
||||
function useEnvShare() {
|
||||
const value = process.env["SHARE"]
|
||||
if (!value) return undefined
|
||||
|
|
@ -578,16 +582,38 @@ async function summarize(response: string) {
|
|||
}
|
||||
}
|
||||
|
||||
async function resolveAgent(): Promise<string | undefined> {
|
||||
const envAgent = useEnvAgent()
|
||||
if (!envAgent) return undefined
|
||||
|
||||
// Validate the agent exists and is a primary agent
|
||||
const agents = await client.agent.list<true>()
|
||||
const agent = agents.data?.find((a) => a.name === envAgent)
|
||||
|
||||
if (!agent) {
|
||||
console.warn(`agent "${envAgent}" not found. Falling back to default agent`)
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (agent.mode === "subagent") {
|
||||
console.warn(`agent "${envAgent}" is a subagent, not a primary agent. Falling back to default agent`)
|
||||
return undefined
|
||||
}
|
||||
|
||||
return envAgent
|
||||
}
|
||||
|
||||
async function chat(text: string, files: PromptFiles = []) {
|
||||
console.log("Sending message to opencode...")
|
||||
const { providerID, modelID } = useEnvModel()
|
||||
const agent = await resolveAgent()
|
||||
|
||||
const chat = await client.session.chat<true>({
|
||||
path: session,
|
||||
body: {
|
||||
providerID,
|
||||
modelID,
|
||||
agent: "build",
|
||||
agent,
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.176",
|
||||
"version": "1.0.184",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.176",
|
||||
"version": "1.0.184",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
<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
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -10,5 +10,6 @@ export default defineConfig({
|
|||
},
|
||||
build: {
|
||||
target: "esnext",
|
||||
sourcemap: true,
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.176",
|
||||
"version": "1.0.184",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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 }],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -229,7 +229,8 @@ function App() {
|
|||
|
||||
let continued = false
|
||||
createEffect(() => {
|
||||
if (continued || sync.status !== "complete" || !args.continue) return
|
||||
// When using -c, session list is loaded in blocking phase, so we can navigate at "partial"
|
||||
if (continued || sync.status === "loading" || !args.continue) return
|
||||
const match = sync.data.session
|
||||
.toSorted((a, b) => b.time.updated - a.time.updated)
|
||||
.find((x) => x.parentID === undefined)?.id
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||
const [agentStore, setAgentStore] = createStore<{
|
||||
current: string
|
||||
}>({
|
||||
current: agents()[0].name,
|
||||
current: agents().find((x) => x.default)?.name ?? agents()[0].name,
|
||||
})
|
||||
const { theme } = useTheme()
|
||||
const colors = createMemo(() => [
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { Binary } from "@opencode-ai/util/binary"
|
|||
import { createSimpleContext } from "./helper"
|
||||
import type { Snapshot } from "@/snapshot"
|
||||
import { useExit } from "./exit"
|
||||
import { useArgs } from "./args"
|
||||
import { batch, onMount } from "solid-js"
|
||||
import { Log } from "@/util/log"
|
||||
import type { Path } from "@opencode-ai/sdk"
|
||||
|
|
@ -254,10 +255,18 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||
})
|
||||
|
||||
const exit = useExit()
|
||||
const args = useArgs()
|
||||
|
||||
async function bootstrap() {
|
||||
// blocking
|
||||
await Promise.all([
|
||||
const sessionListPromise = sdk.client.session.list().then((x) =>
|
||||
setStore(
|
||||
"session",
|
||||
(x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)),
|
||||
),
|
||||
)
|
||||
|
||||
// blocking - include session.list when continuing a session
|
||||
const blockingRequests: Promise<unknown>[] = [
|
||||
sdk.client.config.providers({}, { throwOnError: true }).then((x) => {
|
||||
batch(() => {
|
||||
setStore("provider", x.data!.providers)
|
||||
|
|
@ -271,17 +280,15 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||
}),
|
||||
sdk.client.app.agents({}, { throwOnError: true }).then((x) => setStore("agent", x.data ?? [])),
|
||||
sdk.client.config.get({}, { throwOnError: true }).then((x) => setStore("config", x.data!)),
|
||||
])
|
||||
...(args.continue ? [sessionListPromise] : []),
|
||||
]
|
||||
|
||||
await Promise.all(blockingRequests)
|
||||
.then(() => {
|
||||
if (store.status !== "complete") setStore("status", "partial")
|
||||
// non-blocking
|
||||
Promise.all([
|
||||
sdk.client.session.list().then((x) =>
|
||||
setStore(
|
||||
"session",
|
||||
(x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)),
|
||||
),
|
||||
),
|
||||
...(args.continue ? [] : [sessionListPromise]),
|
||||
sdk.client.command.list().then((x) => setStore("command", x.data ?? [])),
|
||||
sdk.client.lsp.status().then((x) => setStore("lsp", x.data!)),
|
||||
sdk.client.mcp.status().then((x) => setStore("mcp", x.data!)),
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ import { createSimpleContext } from "./helper"
|
|||
import aura from "./theme/aura.json" with { type: "json" }
|
||||
import ayu from "./theme/ayu.json" with { type: "json" }
|
||||
import catppuccin from "./theme/catppuccin.json" with { type: "json" }
|
||||
import catppuccinFrappe from "./theme/catppuccin-frappe.json" with { type: "json" }
|
||||
import catppuccinMacchiato from "./theme/catppuccin-macchiato.json" with { type: "json" }
|
||||
import cobalt2 from "./theme/cobalt2.json" with { type: "json" }
|
||||
import cursor from "./theme/cursor.json" with { type: "json" }
|
||||
import dracula from "./theme/dracula.json" with { type: "json" }
|
||||
import everforest from "./theme/everforest.json" with { type: "json" }
|
||||
import flexoki from "./theme/flexoki.json" with { type: "json" }
|
||||
|
|
@ -136,8 +138,10 @@ export const DEFAULT_THEMES: Record<string, ThemeJson> = {
|
|||
aura,
|
||||
ayu,
|
||||
catppuccin,
|
||||
["catppuccin-frappe"]: catppuccinFrappe,
|
||||
["catppuccin-macchiato"]: catppuccinMacchiato,
|
||||
cobalt2,
|
||||
cursor,
|
||||
dracula,
|
||||
everforest,
|
||||
flexoki,
|
||||
|
|
@ -279,14 +283,23 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||
ready: false,
|
||||
})
|
||||
|
||||
createEffect(async () => {
|
||||
const custom = await getCustomThemes()
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
Object.assign(draft.themes, custom)
|
||||
draft.ready = true
|
||||
}),
|
||||
)
|
||||
createEffect(() => {
|
||||
getCustomThemes()
|
||||
.then((custom) => {
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
Object.assign(draft.themes, custom)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.catch(() => {
|
||||
setStore("active", "opencode")
|
||||
})
|
||||
.finally(() => {
|
||||
if (store.active !== "system") {
|
||||
setStore("ready", true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const renderer = useRenderer()
|
||||
|
|
@ -295,8 +308,25 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||
size: 16,
|
||||
})
|
||||
.then((colors) => {
|
||||
if (!colors.palette[0]) return
|
||||
setStore("themes", "system", generateSystem(colors, store.mode))
|
||||
if (!colors.palette[0]) {
|
||||
if (store.active === "system") {
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
draft.active = "opencode"
|
||||
draft.ready = true
|
||||
}),
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
draft.themes.system = generateSystem(colors, store.mode)
|
||||
if (store.active === "system") {
|
||||
draft.ready = true
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const values = createMemo(() => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,233 @@
|
|||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"frappeRosewater": "#f2d5cf",
|
||||
"frappeFlamingo": "#eebebe",
|
||||
"frappePink": "#f4b8e4",
|
||||
"frappeMauve": "#ca9ee6",
|
||||
"frappeRed": "#e78284",
|
||||
"frappeMaroon": "#ea999c",
|
||||
"frappePeach": "#ef9f76",
|
||||
"frappeYellow": "#e5c890",
|
||||
"frappeGreen": "#a6d189",
|
||||
"frappeTeal": "#81c8be",
|
||||
"frappeSky": "#99d1db",
|
||||
"frappeSapphire": "#85c1dc",
|
||||
"frappeBlue": "#8da4e2",
|
||||
"frappeLavender": "#babbf1",
|
||||
"frappeText": "#c6d0f5",
|
||||
"frappeSubtext1": "#b5bfe2",
|
||||
"frappeSubtext0": "#a5adce",
|
||||
"frappeOverlay2": "#949cb8",
|
||||
"frappeOverlay1": "#838ba7",
|
||||
"frappeOverlay0": "#737994",
|
||||
"frappeSurface2": "#626880",
|
||||
"frappeSurface1": "#51576d",
|
||||
"frappeSurface0": "#414559",
|
||||
"frappeBase": "#303446",
|
||||
"frappeMantle": "#292c3c",
|
||||
"frappeCrust": "#232634"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "frappeBlue",
|
||||
"light": "frappeBlue"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "frappeMauve",
|
||||
"light": "frappeMauve"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "frappePink",
|
||||
"light": "frappePink"
|
||||
},
|
||||
"error": {
|
||||
"dark": "frappeRed",
|
||||
"light": "frappeRed"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "frappeYellow",
|
||||
"light": "frappeYellow"
|
||||
},
|
||||
"success": {
|
||||
"dark": "frappeGreen",
|
||||
"light": "frappeGreen"
|
||||
},
|
||||
"info": {
|
||||
"dark": "frappeTeal",
|
||||
"light": "frappeTeal"
|
||||
},
|
||||
"text": {
|
||||
"dark": "frappeText",
|
||||
"light": "frappeText"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "frappeSubtext1",
|
||||
"light": "frappeSubtext1"
|
||||
},
|
||||
"background": {
|
||||
"dark": "frappeBase",
|
||||
"light": "frappeBase"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "frappeMantle",
|
||||
"light": "frappeMantle"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "frappeCrust",
|
||||
"light": "frappeCrust"
|
||||
},
|
||||
"border": {
|
||||
"dark": "frappeSurface0",
|
||||
"light": "frappeSurface0"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "frappeSurface1",
|
||||
"light": "frappeSurface1"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "frappeSurface2",
|
||||
"light": "frappeSurface2"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "frappeGreen",
|
||||
"light": "frappeGreen"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "frappeRed",
|
||||
"light": "frappeRed"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "frappeOverlay2",
|
||||
"light": "frappeOverlay2"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "frappePeach",
|
||||
"light": "frappePeach"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "frappeGreen",
|
||||
"light": "frappeGreen"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "frappeRed",
|
||||
"light": "frappeRed"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#29342b",
|
||||
"light": "#29342b"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#3a2a31",
|
||||
"light": "#3a2a31"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "frappeMantle",
|
||||
"light": "frappeMantle"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "frappeSurface1",
|
||||
"light": "frappeSurface1"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#223025",
|
||||
"light": "#223025"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#2f242b",
|
||||
"light": "#2f242b"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "frappeText",
|
||||
"light": "frappeText"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "frappeMauve",
|
||||
"light": "frappeMauve"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "frappeBlue",
|
||||
"light": "frappeBlue"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "frappeSky",
|
||||
"light": "frappeSky"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "frappeGreen",
|
||||
"light": "frappeGreen"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "frappeYellow",
|
||||
"light": "frappeYellow"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "frappeYellow",
|
||||
"light": "frappeYellow"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "frappePeach",
|
||||
"light": "frappePeach"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "frappeSubtext0",
|
||||
"light": "frappeSubtext0"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "frappeBlue",
|
||||
"light": "frappeBlue"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "frappeSky",
|
||||
"light": "frappeSky"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "frappeBlue",
|
||||
"light": "frappeBlue"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "frappeSky",
|
||||
"light": "frappeSky"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "frappeText",
|
||||
"light": "frappeText"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "frappeOverlay2",
|
||||
"light": "frappeOverlay2"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "frappeMauve",
|
||||
"light": "frappeMauve"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "frappeBlue",
|
||||
"light": "frappeBlue"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "frappeRed",
|
||||
"light": "frappeRed"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "frappeGreen",
|
||||
"light": "frappeGreen"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "frappePeach",
|
||||
"light": "frappePeach"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "frappeYellow",
|
||||
"light": "frappeYellow"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "frappeSky",
|
||||
"light": "frappeSky"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "frappeText",
|
||||
"light": "frappeText"
|
||||
}
|
||||
}
|
||||
}
|
||||
249
packages/opencode/src/cli/cmd/tui/context/theme/cursor.json
Normal file
249
packages/opencode/src/cli/cmd/tui/context/theme/cursor.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
146
packages/opencode/test/agent/agent.test.ts
Normal file
146
packages/opencode/test/agent/agent.test.ts
Normal 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")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2126,6 +2126,173 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/session/{sessionID}/message/{messageID}/part/{partID}": {
|
||||
"delete": {
|
||||
"operationId": "part.delete",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "directory",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "path",
|
||||
"name": "sessionID",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true,
|
||||
"description": "Session ID"
|
||||
},
|
||||
{
|
||||
"in": "path",
|
||||
"name": "messageID",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true,
|
||||
"description": "Message ID"
|
||||
},
|
||||
{
|
||||
"in": "path",
|
||||
"name": "partID",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true,
|
||||
"description": "Part ID"
|
||||
}
|
||||
],
|
||||
"description": "Delete a part from a message",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successfully deleted part",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/BadRequestError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NotFoundError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-codeSamples": [
|
||||
{
|
||||
"lang": "js",
|
||||
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.part.delete({\n ...\n})"
|
||||
}
|
||||
]
|
||||
},
|
||||
"patch": {
|
||||
"operationId": "part.update",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "directory",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "path",
|
||||
"name": "sessionID",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true,
|
||||
"description": "Session ID"
|
||||
},
|
||||
{
|
||||
"in": "path",
|
||||
"name": "messageID",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true,
|
||||
"description": "Message ID"
|
||||
},
|
||||
{
|
||||
"in": "path",
|
||||
"name": "partID",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true,
|
||||
"description": "Part ID"
|
||||
}
|
||||
],
|
||||
"description": "Update a part in a message",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successfully updated part",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Part"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/BadRequestError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NotFoundError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Part"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-codeSamples": [
|
||||
{
|
||||
"lang": "js",
|
||||
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.part.update({\n ...\n})"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/session/{sessionID}/prompt_async": {
|
||||
"post": {
|
||||
"operationId": "session.prompt_async",
|
||||
|
|
@ -7986,6 +8153,10 @@
|
|||
"description": "Small model to use for tasks like title generation in the format of provider/model",
|
||||
"type": "string"
|
||||
},
|
||||
"default_agent": {
|
||||
"description": "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.",
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"description": "Custom username to display in conversations instead of system username",
|
||||
"type": "string"
|
||||
|
|
@ -8985,6 +9156,9 @@
|
|||
"hidden": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"default": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"topP": {
|
||||
"type": "number"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.176",
|
||||
"version": "1.0.184",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run src/index.ts",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@opencode-ai/tauri",
|
||||
"private": true,
|
||||
"version": "1.0.176",
|
||||
"version": "1.0.184",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo -b",
|
||||
|
|
|
|||
|
|
@ -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}`
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.176",
|
||||
"version": "1.0.184",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./*": "./src/components/*.tsx",
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.0.176",
|
||||
"version": "1.0.184",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
18
packages/web/src/pages/[...slug].md.ts
Normal file
18
packages/web/src/pages/[...slug].md.ts
Normal 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",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue