diff --git a/README.md b/README.md index d0acb758d9..7e20902547 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,8 @@ Русский | العربية | Norsk | - Português (Brasil) + Português (Brasil) | + ไทย

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.th.md b/README.th.md new file mode 100644 index 0000000000..a4b306a6c4 --- /dev/null +++ b/README.th.md @@ -0,0 +1,134 @@ +

+ + + + + OpenCode logo + + +

+

เอเจนต์การเขียนโค้ดด้วย AI แบบโอเพนซอร์ส

+

+ Discord + npm + สถานะการสร้าง +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + العربية | + Norsk | + Português (Brasil) | + ไทย +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### การติดตั้ง + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# ตัวจัดการแพ็กเกจ +npm i -g opencode-ai@latest # หรือ bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS และ Linux (แนะนำ อัปเดตเสมอ) +brew install opencode # macOS และ Linux (brew formula อย่างเป็นทางการ อัปเดตน้อยกว่า) +paru -S opencode-bin # Arch Linux +mise use -g opencode # ระบบปฏิบัติการใดก็ได้ +nix run nixpkgs#opencode # หรือ github:anomalyco/opencode สำหรับสาขาพัฒนาล่าสุด +``` + +> [!TIP] +> ลบเวอร์ชันที่เก่ากว่า 0.1.x ก่อนติดตั้ง + +### แอปพลิเคชันเดสก์ท็อป (เบต้า) + +OpenCode มีให้ใช้งานเป็นแอปพลิเคชันเดสก์ท็อป ดาวน์โหลดโดยตรงจาก [หน้ารุ่น](https://github.com/anomalyco/opencode/releases) หรือ [opencode.ai/download](https://opencode.ai/download) + +| แพลตฟอร์ม | ดาวน์โหลด | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, หรือ AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### ไดเรกทอรีการติดตั้ง + +สคริปต์การติดตั้งจะใช้ลำดับความสำคัญตามเส้นทางการติดตั้ง: + +1. `$OPENCODE_INSTALL_DIR` - ไดเรกทอรีการติดตั้งที่กำหนดเอง +2. `$XDG_BIN_DIR` - เส้นทางที่สอดคล้องกับ XDG Base Directory Specification +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 +``` + +### เอเจนต์ + +OpenCode รวมเอเจนต์ในตัวสองตัวที่คุณสามารถสลับได้ด้วยปุ่ม `Tab` + +- **build** - เอเจนต์เริ่มต้น มีสิทธิ์เข้าถึงแบบเต็มสำหรับงานพัฒนา +- **plan** - เอเจนต์อ่านอย่างเดียวสำหรับการวิเคราะห์และการสำรวจโค้ด + - ปฏิเสธการแก้ไขไฟล์โดยค่าเริ่มต้น + - ขอสิทธิ์ก่อนเรียกใช้คำสั่ง bash + - เหมาะสำหรับสำรวจโค้ดเบสที่ไม่คุ้นเคยหรือวางแผนการเปลี่ยนแปลง + +นอกจากนี้ยังมีเอเจนต์ย่อย **general** สำหรับการค้นหาที่ซับซ้อนและงานหลายขั้นตอน +ใช้ภายในและสามารถเรียกใช้ได้โดยใช้ `@general` ในข้อความ + +เรียนรู้เพิ่มเติมเกี่ยวกับ [เอเจนต์](https://opencode.ai/docs/agents) + +### เอกสารประกอบ + +สำหรับข้อมูลเพิ่มเติมเกี่ยวกับวิธีกำหนดค่า OpenCode [**ไปที่เอกสารของเรา**](https://opencode.ai/docs) + +### การมีส่วนร่วม + +หากคุณสนใจที่จะมีส่วนร่วมใน OpenCode โปรดอ่าน [เอกสารการมีส่วนร่วม](./CONTRIBUTING.md) ก่อนส่ง Pull Request + +### การสร้างบน OpenCode + +หากคุณทำงานในโปรเจกต์ที่เกี่ยวข้องกับ OpenCode และใช้ "opencode" เป็นส่วนหนึ่งของชื่อ เช่น "opencode-dashboard" หรือ "opencode-mobile" โปรดเพิ่มหมายเหตุใน README ของคุณเพื่อชี้แจงว่าไม่ได้สร้างโดยทีม OpenCode และไม่ได้เกี่ยวข้องกับเราในทางใด + +### คำถามที่พบบ่อย + +#### ต่างจาก Claude Code อย่างไร? + +คล้ายกับ Claude Code มากในแง่ความสามารถ นี่คือความแตกต่างหลัก: + +- โอเพนซอร์ส 100% +- ไม่ผูกมัดกับผู้ให้บริการใดๆ แม้ว่าเราจะแนะนำโมเดลที่เราจัดหาให้ผ่าน [OpenCode Zen](https://opencode.ai/zen) OpenCode สามารถใช้กับ Claude, OpenAI, Google หรือแม้กระทั่งโมเดลในเครื่องได้ เมื่อโมเดลพัฒนาช่องว่างระหว่างพวกมันจะปิดลงและราคาจะลดลง ดังนั้นการไม่ผูกมัดกับผู้ให้บริการจึงสำคัญ +- รองรับ LSP ใช้งานได้ทันทีหลังการติดตั้งโดยไม่ต้องปรับแต่งหรือเปลี่ยนแปลงฟังก์ชันการทำงานใด ๆ +- เน้นที่ TUI OpenCode สร้างโดยผู้ใช้ neovim และผู้สร้าง [terminal.shop](https://terminal.shop) เราจะผลักดันขีดจำกัดของสิ่งที่เป็นไปได้ในเทอร์มินัล +- สถาปัตยกรรมไคลเอนต์/เซิร์ฟเวอร์ ตัวอย่างเช่น อาจอนุญาตให้ OpenCode ทำงานบนคอมพิวเตอร์ของคุณ ในขณะที่คุณสามารถขับเคลื่อนจากระยะไกลผ่านแอปมือถือ หมายความว่า TUI frontend เป็นหนึ่งในไคลเอนต์ที่เป็นไปได้เท่านั้น + +--- + +**ร่วมชุมชนของเรา** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/STATS.md b/STATS.md index f00ebd72a3..01be7f3d20 100644 --- a/STATS.md +++ b/STATS.md @@ -213,3 +213,4 @@ | 2026-01-25 | 6,639,082 (+268,063) | 2,187,853 (+30,983) | 8,826,935 (+299,046) | | 2026-01-26 | 6,941,620 (+302,538) | 2,232,115 (+44,262) | 9,173,735 (+346,800) | | 2026-01-27 | 7,208,093 (+266,473) | 2,280,762 (+48,647) | 9,488,855 (+315,120) | +| 2026-01-28 | 7,489,370 (+281,277) | 2,314,849 (+34,087) | 9,804,219 (+315,364) | diff --git a/bun.lock b/bun.lock index 96c3f952be..2651cd4943 100644 --- a/bun.lock +++ b/bun.lock @@ -23,7 +23,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.1.36", + "version": "1.1.40", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -73,7 +73,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.1.36", + "version": "1.1.40", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -107,7 +107,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.1.36", + "version": "1.1.40", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -134,7 +134,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.1.36", + "version": "1.1.40", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -158,7 +158,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.1.36", + "version": "1.1.40", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -182,7 +182,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.1.36", + "version": "1.1.40", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -212,7 +212,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.1.36", + "version": "1.1.40", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -241,7 +241,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.1.36", + "version": "1.1.40", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -257,7 +257,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.1.36", + "version": "1.1.40", "bin": { "opencode": "./bin/opencode", }, @@ -364,7 +364,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.1.36", + "version": "1.1.40", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -384,7 +384,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.1.36", + "version": "1.1.40", "devDependencies": { "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", @@ -395,7 +395,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.1.36", + "version": "1.1.40", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -408,7 +408,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.1.36", + "version": "1.1.40", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -450,7 +450,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.1.36", + "version": "1.1.40", "dependencies": { "zod": "catalog:", }, @@ -461,7 +461,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.1.36", + "version": "1.1.40", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 8284234c5c..887b6498cc 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.1.36", + "version": "1.1.40", "description": "", "type": "module", "exports": { diff --git a/packages/app/src/components/dialog-custom-provider.tsx b/packages/app/src/components/dialog-custom-provider.tsx new file mode 100644 index 0000000000..28a947f3b3 --- /dev/null +++ b/packages/app/src/components/dialog-custom-provider.tsx @@ -0,0 +1,424 @@ +import { Button } from "@opencode-ai/ui/button" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { Dialog } from "@opencode-ai/ui/dialog" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { ProviderIcon } from "@opencode-ai/ui/provider-icon" +import { TextField } from "@opencode-ai/ui/text-field" +import { showToast } from "@opencode-ai/ui/toast" +import { For } from "solid-js" +import { createStore, produce } from "solid-js/store" +import { Link } from "@/components/link" +import { useGlobalSDK } from "@/context/global-sdk" +import { useGlobalSync } from "@/context/global-sync" +import { useLanguage } from "@/context/language" +import { DialogSelectProvider } from "./dialog-select-provider" + +const PROVIDER_ID = /^[a-z0-9][a-z0-9-_]*$/ +const OPENAI_COMPATIBLE = "@ai-sdk/openai-compatible" + +type Props = { + back?: "providers" | "close" +} + +export function DialogCustomProvider(props: Props) { + const dialog = useDialog() + const globalSync = useGlobalSync() + const globalSDK = useGlobalSDK() + const language = useLanguage() + + const [form, setForm] = createStore({ + providerID: "", + name: "", + baseURL: "", + apiKey: "", + models: [{ id: "", name: "" }], + headers: [{ key: "", value: "" }], + saving: false, + }) + + const [errors, setErrors] = createStore({ + providerID: undefined as string | undefined, + name: undefined as string | undefined, + baseURL: undefined as string | undefined, + models: [{} as { id?: string; name?: string }], + headers: [{} as { key?: string; value?: string }], + }) + + const goBack = () => { + if (props.back === "close") { + dialog.close() + return + } + dialog.show(() => ) + } + + const addModel = () => { + setForm( + "models", + produce((draft) => { + draft.push({ id: "", name: "" }) + }), + ) + setErrors( + "models", + produce((draft) => { + draft.push({}) + }), + ) + } + + const removeModel = (index: number) => { + if (form.models.length <= 1) return + setForm( + "models", + produce((draft) => { + draft.splice(index, 1) + }), + ) + setErrors( + "models", + produce((draft) => { + draft.splice(index, 1) + }), + ) + } + + const addHeader = () => { + setForm( + "headers", + produce((draft) => { + draft.push({ key: "", value: "" }) + }), + ) + setErrors( + "headers", + produce((draft) => { + draft.push({}) + }), + ) + } + + const removeHeader = (index: number) => { + if (form.headers.length <= 1) return + setForm( + "headers", + produce((draft) => { + draft.splice(index, 1) + }), + ) + setErrors( + "headers", + produce((draft) => { + draft.splice(index, 1) + }), + ) + } + + const validate = () => { + const providerID = form.providerID.trim() + const name = form.name.trim() + const baseURL = form.baseURL.trim() + const apiKey = form.apiKey.trim() + + const env = apiKey.match(/^\{env:([^}]+)\}$/)?.[1]?.trim() + const key = apiKey && !env ? apiKey : undefined + + const idError = !providerID + ? "Provider ID is required" + : !PROVIDER_ID.test(providerID) + ? "Use lowercase letters, numbers, hyphens, or underscores" + : undefined + + const nameError = !name ? "Display name is required" : undefined + const urlError = !baseURL + ? "Base URL is required" + : !/^https?:\/\//.test(baseURL) + ? "Must start with http:// or https://" + : undefined + + const disabled = (globalSync.data.config.disabled_providers ?? []).includes(providerID) + const existingProvider = globalSync.data.provider.all.find((p) => p.id === providerID) + const existsError = idError + ? undefined + : existingProvider && !disabled + ? "That provider ID already exists" + : undefined + + const seenModels = new Set() + const modelErrors = form.models.map((m) => { + const id = m.id.trim() + const modelIdError = !id + ? "Required" + : seenModels.has(id) + ? "Duplicate" + : (() => { + seenModels.add(id) + return undefined + })() + const modelNameError = !m.name.trim() ? "Required" : undefined + return { id: modelIdError, name: modelNameError } + }) + const modelsValid = modelErrors.every((m) => !m.id && !m.name) + const models = Object.fromEntries(form.models.map((m) => [m.id.trim(), { name: m.name.trim() }])) + + const seenHeaders = new Set() + const headerErrors = form.headers.map((h) => { + const key = h.key.trim() + const value = h.value.trim() + + if (!key && !value) return {} + const keyError = !key + ? "Required" + : seenHeaders.has(key.toLowerCase()) + ? "Duplicate" + : (() => { + seenHeaders.add(key.toLowerCase()) + return undefined + })() + const valueError = !value ? "Required" : undefined + return { key: keyError, value: valueError } + }) + const headersValid = headerErrors.every((h) => !h.key && !h.value) + const headers = Object.fromEntries( + form.headers + .map((h) => ({ key: h.key.trim(), value: h.value.trim() })) + .filter((h) => !!h.key && !!h.value) + .map((h) => [h.key, h.value]), + ) + + setErrors( + produce((draft) => { + draft.providerID = idError ?? existsError + draft.name = nameError + draft.baseURL = urlError + draft.models = modelErrors + draft.headers = headerErrors + }), + ) + + const ok = !idError && !existsError && !nameError && !urlError && modelsValid && headersValid + if (!ok) return + + const options = { + baseURL, + ...(Object.keys(headers).length ? { headers } : {}), + } + + return { + providerID, + name, + key, + config: { + npm: OPENAI_COMPATIBLE, + name, + ...(env ? { env: [env] } : {}), + options, + models, + }, + } + } + + const save = async (e: SubmitEvent) => { + e.preventDefault() + if (form.saving) return + + const result = validate() + if (!result) return + + setForm("saving", true) + + const disabledProviders = globalSync.data.config.disabled_providers ?? [] + const nextDisabled = disabledProviders.filter((id) => id !== result.providerID) + + const auth = result.key + ? globalSDK.client.auth.set({ + providerID: result.providerID, + auth: { + type: "api", + key: result.key, + }, + }) + : Promise.resolve() + + auth + .then(() => + globalSync.updateConfig({ provider: { [result.providerID]: result.config }, disabled_providers: nextDisabled }), + ) + .then(() => { + dialog.close() + showToast({ + variant: "success", + icon: "circle-check", + title: language.t("provider.connect.toast.connected.title", { provider: result.name }), + description: language.t("provider.connect.toast.connected.description", { provider: result.name }), + }) + }) + .catch((err: unknown) => { + const message = err instanceof Error ? err.message : String(err) + showToast({ title: language.t("common.requestFailed"), description: message }) + }) + .finally(() => { + setForm("saving", false) + }) + } + + return ( + + } + transition + > +
+
+ +
Custom provider
+
+ +
+

+ Configure an OpenAI-compatible provider. See the{" "} + + provider config docs + + . +

+ +
+ + + + +
+ +
+ + + {(m, i) => ( +
+
+ setForm("models", i(), "id", v)} + validationState={errors.models[i()]?.id ? "invalid" : undefined} + error={errors.models[i()]?.id} + /> +
+
+ setForm("models", i(), "name", v)} + validationState={errors.models[i()]?.name ? "invalid" : undefined} + error={errors.models[i()]?.name} + /> +
+ removeModel(i())} + disabled={form.models.length <= 1} + aria-label="Remove model" + /> +
+ )} +
+ +
+ +
+ + + {(h, i) => ( +
+
+ setForm("headers", i(), "key", v)} + validationState={errors.headers[i()]?.key ? "invalid" : undefined} + error={errors.headers[i()]?.key} + /> +
+
+ setForm("headers", i(), "value", v)} + validationState={errors.headers[i()]?.value ? "invalid" : undefined} + error={errors.headers[i()]?.value} + /> +
+ removeHeader(i())} + disabled={form.headers.length <= 1} + aria-label="Remove header" + /> +
+ )} +
+ +
+ + +
+
+
+ ) +} diff --git a/packages/app/src/components/dialog-manage-models.tsx b/packages/app/src/components/dialog-manage-models.tsx index 1ecefa2cbb..9ee48736ca 100644 --- a/packages/app/src/components/dialog-manage-models.tsx +++ b/packages/app/src/components/dialog-manage-models.tsx @@ -1,16 +1,33 @@ import { Dialog } from "@opencode-ai/ui/dialog" import { List } from "@opencode-ai/ui/list" import { Switch } from "@opencode-ai/ui/switch" +import { Button } from "@opencode-ai/ui/button" import type { Component } from "solid-js" import { useLocal } from "@/context/local" import { popularProviders } from "@/hooks/use-providers" import { useLanguage } from "@/context/language" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { DialogSelectProvider } from "./dialog-select-provider" export const DialogManageModels: Component = () => { const local = useLocal() const language = useLanguage() + const dialog = useDialog() + + const handleConnectProvider = () => { + dialog.show(() => ) + } + return ( - + + {language.t("command.provider.connect")} + + } + > } > -
+
{item.title} diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index e927ae4fd1..4f0dcc3ee6 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -187,7 +187,7 @@ export function ModelSelectorPopover(props: { setStore("content", el)} - class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden" + class="w-72 h-80 flex flex-col p-2 rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden" onEscapeKeyDown={(event) => { setStore("dismiss", "escape") setStore("open", false) diff --git a/packages/app/src/components/dialog-select-provider.tsx b/packages/app/src/components/dialog-select-provider.tsx index 5933bff197..f878e50e81 100644 --- a/packages/app/src/components/dialog-select-provider.tsx +++ b/packages/app/src/components/dialog-select-provider.tsx @@ -5,9 +5,17 @@ import { Dialog } from "@opencode-ai/ui/dialog" import { List } from "@opencode-ai/ui/list" import { Tag } from "@opencode-ai/ui/tag" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" -import { IconName } from "@opencode-ai/ui/icons/provider" +import { iconNames, type IconName } from "@opencode-ai/ui/icons/provider" import { DialogConnectProvider } from "./dialog-connect-provider" import { useLanguage } from "@/context/language" +import { DialogCustomProvider } from "./dialog-custom-provider" + +const CUSTOM_ID = "_custom" + +function icon(id: string): IconName { + if (iconNames.includes(id as IconName)) return id as IconName + return "synthetic" +} export const DialogSelectProvider: Component = () => { const dialog = useDialog() @@ -26,11 +34,13 @@ export const DialogSelectProvider: Component = () => { key={(x) => x?.id} items={() => { language.locale() - return providers.all() + return [{ id: CUSTOM_ID, name: "Custom provider" }, ...providers.all()] }} filterKeys={["id", "name"]} groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())} sortBy={(a, b) => { + if (a.id === CUSTOM_ID) return -1 + if (b.id === CUSTOM_ID) return 1 if (popularProviders.includes(a.id) && popularProviders.includes(b.id)) return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id) return a.name.localeCompare(b.name) @@ -43,13 +53,20 @@ export const DialogSelectProvider: Component = () => { }} onSelect={(x) => { if (!x) return + if (x.id === CUSTOM_ID) { + dialog.show(() => ) + return + } dialog.show(() => ) }} > {(i) => (
- + {i.name} + + {language.t("settings.providers.tag.custom")} + {language.t("dialog.provider.tag.recommended")} diff --git a/packages/app/src/components/file-tree.tsx b/packages/app/src/components/file-tree.tsx index bd989f755d..d43310b195 100644 --- a/packages/app/src/components/file-tree.tsx +++ b/packages/app/src/components/file-tree.tsx @@ -8,7 +8,6 @@ import { createMemo, For, Match, - onCleanup, Show, splitProps, Switch, @@ -124,28 +123,7 @@ export default function FileTree(props: { createEffect(() => { const path = props.path - const state = { cancelled: false, timer: undefined as number | undefined } - - const load = (attempt: number) => { - if (state.cancelled) return - if (file.tree.state(path)?.loaded) return - - void untrack(() => file.tree.list(path)).finally(() => { - if (state.cancelled) return - if (file.tree.state(path)?.loaded) return - if (attempt >= 2) return - - const wait = Math.min(2000, 250 * 2 ** attempt) - state.timer = window.setTimeout(() => load(attempt + 1), wait) - }) - } - - load(0) - - onCleanup(() => { - state.cancelled = true - if (state.timer !== undefined) clearTimeout(state.timer) - }) + untrack(() => void file.tree.list(path)) }) const nodes = createMemo(() => { diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 9f038b6e83..4d227f44b6 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -189,11 +189,13 @@ export const PromptInput: Component = (props) => { const wantsReview = item.commentOrigin === "review" || (item.commentOrigin !== "file" && commentInReview(item.path)) if (wantsReview) { + layout.fileTree.open() layout.fileTree.setTab("changes") requestAnimationFrame(() => comments.setFocus(focus)) return } + layout.fileTree.open() layout.fileTree.setTab("all") const tab = files.tab(item.path) tabs().open(tab) @@ -1036,13 +1038,17 @@ export const PromptInput: Component = (props) => { return } + const ctrl = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey + if (store.popover) { if (event.key === "Tab") { selectPopoverActive() event.preventDefault() return } - if (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter") { + const nav = event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter" + const ctrlNav = ctrl && (event.key === "n" || event.key === "p") + if (nav || ctrlNav) { if (store.popover === "at") { atOnKeyDown(event) event.preventDefault() @@ -1056,8 +1062,6 @@ export const PromptInput: Component = (props) => { } } - const ctrl = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey - if (ctrl && event.code === "KeyG") { if (store.popover) { setStore("popover", null) diff --git a/packages/app/src/components/session-context-usage.tsx b/packages/app/src/components/session-context-usage.tsx index afdb18bb09..1e37d8f6a2 100644 --- a/packages/app/src/components/session-context-usage.tsx +++ b/packages/app/src/components/session-context-usage.tsx @@ -57,6 +57,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) { const openContext = () => { if (!params.id) return + layout.fileTree.open() layout.fileTree.setTab("all") tabs().open("context") tabs().setActive("context") diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 9fddb4507c..d794fb5c63 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -280,17 +280,14 @@ export function SessionHeader() {
+ ) + createEffect( on( () => tabs().active(), @@ -1252,37 +1292,21 @@ export default function Page() { const id = params.id if (!id) return - const wants = isDesktop() ? fileTreeTab() === "changes" : store.mobileTab === "changes" + const wants = isDesktop() ? layout.fileTree.opened() && fileTreeTab() === "changes" : store.mobileTab === "changes" if (!wants) return if (sync.data.session_diff[id] !== undefined) return + if (sync.status === "loading") return - const state = { - cancelled: false, - attempt: 0, - timer: undefined as number | undefined, - } + void sync.session.diff(id) + }) - const load = () => { - if (state.cancelled) return - const pending = sync.session.diff(id) - if (!pending) return - pending.catch(() => { - if (state.cancelled) return - const attempt = state.attempt + 1 - state.attempt = attempt - if (attempt > 5) return - if (state.timer !== undefined) clearTimeout(state.timer) - const wait = Math.min(10000, 250 * 2 ** (attempt - 1)) - state.timer = window.setTimeout(load, wait) - }) - } + createEffect(() => { + if (!isDesktop()) return + if (!layout.fileTree.opened()) return + if (sync.status === "loading") return - load() - - onCleanup(() => { - state.cancelled = true - if (state.timer !== undefined) clearTimeout(state.timer) - }) + fileTreeTab() + void file.tree.list("") }) const autoScroll = createAutoScroll({ @@ -1290,9 +1314,15 @@ export default function Page() { overflowAnchor: "dynamic", }) + const clearMessageHash = () => { + if (!window.location.hash) return + window.history.replaceState(null, "", window.location.href.replace(/#.*$/, "")) + } + const resumeScroll = () => { setStore("messageId", undefined) autoScroll.forceScrollToBottom() + clearMessageHash() } // When the user returns to the bottom, treat the active message as "latest". @@ -1302,6 +1332,7 @@ export default function Page() { (scrolled) => { if (scrolled) return setStore("messageId", undefined) + clearMessageHash() }, { defer: true }, ), @@ -1377,7 +1408,6 @@ export default function Page() { requestAnimationFrame(() => { const delta = el.scrollHeight - beforeHeight if (!delta) return - markScrollIgnore() el.scrollTop = beforeTop + delta }) @@ -1415,7 +1445,6 @@ export default function Page() { if (stick && el) { requestAnimationFrame(() => { - markScrollIgnore() el.scrollTo({ top: el.scrollHeight, behavior: "auto" }) }) } @@ -1510,6 +1539,7 @@ export default function Page() { const match = hash.match(/^message-(.+)$/) if (match) { + autoScroll.pause() const msg = visibleUserMessages().find((m) => m.id === match[1]) if (msg) { scrollToMessage(msg, behavior) @@ -1523,6 +1553,7 @@ export default function Page() { const target = document.getElementById(hash) if (target) { + autoScroll.pause() scrollToElement(target, behavior) return } @@ -1619,6 +1650,7 @@ export default function Page() { const msg = visibleUserMessages().find((m) => m.id === targetId) if (!msg) return if (ui.pendingMessage === targetId) setUi("pendingMessage", undefined) + autoScroll.pause() requestAnimationFrame(() => scrollToMessage(msg, "auto")) }) @@ -1729,10 +1761,11 @@ export default function Page() {
@@ -1799,28 +1832,102 @@ export default function Page() { >
markScrollGesture(e.target)} - onTouchMove={(e) => markScrollGesture(e.target)} + onWheel={(e) => { + const root = e.currentTarget + const target = e.target instanceof Element ? e.target : undefined + const nested = target?.closest("[data-scrollable]") + if (!nested || nested === root) { + markScrollGesture(root) + return + } + + if (!(nested instanceof HTMLElement)) { + markScrollGesture(root) + return + } + + const max = nested.scrollHeight - nested.clientHeight + if (max <= 1) { + markScrollGesture(root) + return + } + + const delta = + e.deltaMode === 1 + ? e.deltaY * 40 + : e.deltaMode === 2 + ? e.deltaY * root.clientHeight + : e.deltaY + if (!delta) return + + if (delta < 0) { + if (nested.scrollTop + delta <= 0) markScrollGesture(root) + return + } + + const remaining = max - nested.scrollTop + if (delta > remaining) markScrollGesture(root) + }} + onTouchStart={(e) => { + touchGesture = e.touches[0]?.clientY + }} + onTouchMove={(e) => { + const next = e.touches[0]?.clientY + const prev = touchGesture + touchGesture = next + if (next === undefined || prev === undefined) return + + const delta = prev - next + if (!delta) return + + const root = e.currentTarget + const target = e.target instanceof Element ? e.target : undefined + const nested = target?.closest("[data-scrollable]") + if (!nested || nested === root) { + markScrollGesture(root) + return + } + + if (!(nested instanceof HTMLElement)) { + markScrollGesture(root) + return + } + + const max = nested.scrollHeight - nested.clientHeight + if (max <= 1) { + markScrollGesture(root) + return + } + + if (delta < 0) { + if (nested.scrollTop + delta <= 0) markScrollGesture(root) + return + } + + const remaining = max - nested.scrollTop + if (delta > remaining) markScrollGesture(root) + }} + onTouchEnd={() => { + touchGesture = undefined + }} + onTouchCancel={() => { + touchGesture = undefined + }} onPointerDown={(e) => { if (e.target !== e.currentTarget) return - markScrollGesture(e.target) + markScrollGesture(e.currentTarget) }} onScroll={(e) => { - const gesture = hasScrollGesture() - if (!hasScrollIgnore() || gesture) autoScroll.handleScroll() - if (!gesture) return - markScrollGesture(e.target) + if (!hasScrollGesture()) return + autoScroll.handleScroll() + markScrollGesture(e.currentTarget) if (isDesktop()) scheduleScrollSpy(e.currentTarget) }} onClick={autoScroll.handleInteraction} @@ -1833,6 +1940,7 @@ export default function Page() { "sticky top-0 z-30 bg-background-stronger": true, "w-full": true, "px-4 md:px-6": true, + "md:max-w-200 md:mx-auto": centered(), }} >
@@ -1857,7 +1965,13 @@ export default function Page() {
0}>
@@ -1905,7 +2019,10 @@ export default function Page() {
(promptDock = el)} class="absolute inset-x-0 bottom-0 pt-12 pb-4 flex flex-col justify-center items-center z-50 px-4 md:px-0 bg-gradient-to-t from-background-stronger via-background-stronger to-transparent pointer-events-none" > -
+
{(perm) => (
@@ -2029,7 +2151,7 @@ export default function Page() {
- + {/* Desktop side panel - hidden on mobile */} - +
+ {reviewPanel()}
diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 169f9a7e89..4bc1d0ef85 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.1.36", + "version": "1.1.40", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 515ae15316..bb74c029b9 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.1.36", + "version": "1.1.40", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 7202c4cfae..b3836dcf83 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.1.36", + "version": "1.1.40", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index fc980b6fbf..4499061c72 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.1.36", + "version": "1.1.40", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 49e032339c..e960d21917 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.1.36", + "version": "1.1.40", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 1a216c88f9..6eef4b810c 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.1.36", + "version": "1.1.40", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 7c43d144b9..26d8045edb 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.1.36" +version = "1.1.40" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.36/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.40/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.36/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.40/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.36/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.40/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.36/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.40/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.36/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.40/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 90abb35428..c48548cb76 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.1.36", + "version": "1.1.40", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index b371d847c6..15d61f2a64 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.1.36", + "version": "1.1.40", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index 3fd2830536..ce948b92ac 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -1,6 +1,5 @@ import path from "path" import { Global } from "../global" -import fs from "fs/promises" import z from "zod" export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key" @@ -59,15 +58,13 @@ export namespace Auth { export async function set(key: string, info: Info) { const file = Bun.file(filepath) const data = await all() - await Bun.write(file, JSON.stringify({ ...data, [key]: info }, null, 2)) - await fs.chmod(file.name!, 0o600) + await Bun.write(file, JSON.stringify({ ...data, [key]: info }, null, 2), { mode: 0o600 }) } export async function remove(key: string) { const file = Bun.file(filepath) const data = await all() delete data[key] - await Bun.write(file, JSON.stringify(data, null, 2)) - await fs.chmod(file.name!, 0o600) + await Bun.write(file, JSON.stringify(data, null, 2), { mode: 0o600 }) } } diff --git a/packages/opencode/src/cli/cmd/tui/component/logo.tsx b/packages/opencode/src/cli/cmd/tui/component/logo.tsx index 771962b75d..8e6208b140 100644 --- a/packages/opencode/src/cli/cmd/tui/component/logo.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/logo.tsx @@ -1,16 +1,13 @@ import { TextAttributes, RGBA } from "@opentui/core" import { For, type JSX } from "solid-js" import { useTheme, tint } from "@tui/context/theme" +import { logo, marks } from "@/cli/logo" // Shadow markers (rendered chars in parens): // _ = full shadow cell (space with bg=shadow) // ^ = letter top, shadow bottom (▀ with fg=letter, bg=shadow) // ~ = shadow top only (▀ with fg=shadow) -const SHADOW_MARKER = /[_^~]/ - -const LOGO_LEFT = [` `, `█▀▀█ █▀▀█ █▀▀█ █▀▀▄`, `█__█ █__█ █^^^ █__█`, `▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀~~▀`] - -const LOGO_RIGHT = [` ▄ `, `█▀▀▀ █▀▀█ █▀▀█ █▀▀█`, `█___ █__█ █__█ █^^^`, `▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`] +const SHADOW_MARKER = new RegExp(`[${marks}]`) export function Logo() { const { theme } = useTheme() @@ -75,11 +72,11 @@ export function Logo() { return ( - + {(line, index) => ( {renderLine(line, theme.textMuted, false)} - {renderLine(LOGO_RIGHT[index()], theme.text, true)} + {renderLine(logo.right[index()], theme.text, true)} )} diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 6f6ae33d2f..496757d32c 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -58,6 +58,7 @@ import { DialogTimeline } from "./dialog-timeline" import { DialogForkFromTimeline } from "./dialog-fork-from-timeline" import { DialogSessionRename } from "../../component/dialog-session-rename" import { Sidebar } from "./sidebar" +import { Flag } from "@/flag/flag" import { LANGUAGE_EXTENSIONS } from "@/lsp/language" import parsers from "../../../../../../parsers-config.ts" import { Clipboard } from "../../util/clipboard" @@ -1338,15 +1339,27 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess return ( - + + + + + + + + ) diff --git a/packages/opencode/src/cli/logo.ts b/packages/opencode/src/cli/logo.ts new file mode 100644 index 0000000000..44fb93c15b --- /dev/null +++ b/packages/opencode/src/cli/logo.ts @@ -0,0 +1,6 @@ +export const logo = { + left: [" ", "█▀▀█ █▀▀█ █▀▀█ █▀▀▄", "█__█ █__█ █^^^ █__█", "▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀~~▀"], + right: [" ▄ ", "█▀▀▀ █▀▀█ █▀▀█ █▀▀█", "█___ █__█ █__█ █^^^", "▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀"], +} + +export const marks = "_^~" diff --git a/packages/opencode/src/cli/ui.ts b/packages/opencode/src/cli/ui.ts index acd1383a07..9df1f4ac55 100644 --- a/packages/opencode/src/cli/ui.ts +++ b/packages/opencode/src/cli/ui.ts @@ -1,15 +1,9 @@ import z from "zod" import { EOL } from "os" import { NamedError } from "@opencode-ai/util/error" +import { logo as glyphs } from "./logo" export namespace UI { - const LOGO = [ - [`  `, ` ▄ `], - [`█▀▀█ █▀▀█ █▀▀█ █▀▀▄ `, `█▀▀▀ █▀▀█ █▀▀█ █▀▀█`], - [`█░░█ █░░█ █▀▀▀ █░░█ `, `█░░░ █░░█ █░░█ █▀▀▀`], - [`▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ `, `▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`], - ] - export const CancelledError = NamedError.create("UICancelledError", z.void()) export const Style = { @@ -47,15 +41,50 @@ export namespace UI { } export function logo(pad?: string) { - const result = [] - for (const row of LOGO) { - if (pad) result.push(pad) - result.push(Bun.color("gray", "ansi")) - result.push(row[0]) - result.push("\x1b[0m") - result.push(row[1]) - result.push(EOL) + const result: string[] = [] + const reset = "\x1b[0m" + const left = { + fg: Bun.color("gray", "ansi") ?? "", + shadow: "\x1b[38;5;235m", + bg: "\x1b[48;5;235m", } + const right = { + fg: reset, + shadow: "\x1b[38;5;238m", + bg: "\x1b[48;5;238m", + } + const gap = " " + const draw = (line: string, fg: string, shadow: string, bg: string) => { + const parts: string[] = [] + for (const char of line) { + if (char === "_") { + parts.push(bg, " ", reset) + continue + } + if (char === "^") { + parts.push(fg, bg, "▀", reset) + continue + } + if (char === "~") { + parts.push(shadow, "▀", reset) + continue + } + if (char === " ") { + parts.push(" ") + continue + } + parts.push(fg, char, reset) + } + return parts.join("") + } + glyphs.left.forEach((row, index) => { + if (pad) result.push(pad) + result.push(draw(row, left.fg, left.shadow, left.bg)) + result.push(gap) + const other = glyphs.right[index] ?? "" + result.push(draw(other, right.fg, right.shadow, right.bg)) + result.push(EOL) + }) return result.join("").trimEnd() } diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 020e626cba..8c65726e23 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1104,20 +1104,23 @@ export namespace Config { mergeDeep(await loadFile(path.join(Global.Path.config, "opencode.jsonc"))), ) - await import(path.join(Global.Path.config, "config"), { - with: { - type: "toml", - }, - }) - .then(async (mod) => { - const { provider, model, ...rest } = mod.default - if (provider && model) result.model = `${provider}/${model}` - result["$schema"] = "https://opencode.ai/config.json" - result = mergeDeep(result, rest) - await Bun.write(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2)) - await fs.unlink(path.join(Global.Path.config, "config")) + const legacy = path.join(Global.Path.config, "config") + if (existsSync(legacy)) { + await import(pathToFileURL(legacy).href, { + with: { + type: "toml", + }, }) - .catch(() => {}) + .then(async (mod) => { + const { provider, model, ...rest } = mod.default + if (provider && model) result.model = `${provider}/${model}` + result["$schema"] = "https://opencode.ai/config.json" + result = mergeDeep(result, rest) + await Bun.write(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2)) + await fs.unlink(legacy) + }) + .catch(() => {}) + } return result }) @@ -1341,24 +1344,35 @@ export namespace Config { throw new JsonError({ path: filepath }, { cause: err }) }) - if (!filepath.endsWith(".jsonc")) { - const existing = parseConfig(before, filepath) - await Bun.write(filepath, JSON.stringify(mergeDeep(existing, config), null, 2)) - } else { - const next = patchJsonc(before, config) - parseConfig(next, filepath) - await Bun.write(filepath, next) - } + const next = await (async () => { + if (!filepath.endsWith(".jsonc")) { + const existing = parseConfig(before, filepath) + const merged = mergeDeep(existing, config) + await Bun.write(filepath, JSON.stringify(merged, null, 2)) + return merged + } + + const updated = patchJsonc(before, config) + const merged = parseConfig(updated, filepath) + await Bun.write(filepath, updated) + return merged + })() global.reset() - await Instance.disposeAll() - GlobalBus.emit("event", { - directory: "global", - payload: { - type: Event.Disposed.type, - properties: {}, - }, - }) + + void Instance.disposeAll() + .catch(() => undefined) + .finally(() => { + GlobalBus.emit("event", { + directory: "global", + payload: { + type: Event.Disposed.type, + properties: {}, + }, + }) + }) + + return next } export async function directories() { diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 0274dcc82b..9dea041e46 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -46,6 +46,7 @@ export namespace Flag { export const OPENCODE_EXPERIMENTAL_LSP_TOOL = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_LSP_TOOL") export const OPENCODE_DISABLE_FILETIME_CHECK = truthy("OPENCODE_DISABLE_FILETIME_CHECK") export const OPENCODE_EXPERIMENTAL_PLAN_MODE = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_PLAN_MODE") + export const OPENCODE_EXPERIMENTAL_MARKDOWN = truthy("OPENCODE_EXPERIMENTAL_MARKDOWN") export const OPENCODE_MODELS_URL = process.env["OPENCODE_MODELS_URL"] function number(key: string) { diff --git a/packages/opencode/src/global/index.ts b/packages/opencode/src/global/index.ts index ade3e5d529..10b6125a6a 100644 --- a/packages/opencode/src/global/index.ts +++ b/packages/opencode/src/global/index.ts @@ -33,7 +33,7 @@ await Promise.all([ fs.mkdir(Global.Path.bin, { recursive: true }), ]) -const CACHE_VERSION = "19" +const CACHE_VERSION = "21" const version = await Bun.file(path.join(Global.Path.cache, "version")) .text() diff --git a/packages/opencode/src/mcp/auth.ts b/packages/opencode/src/mcp/auth.ts index 7f7dbd156c..0f91a35b87 100644 --- a/packages/opencode/src/mcp/auth.ts +++ b/packages/opencode/src/mcp/auth.ts @@ -1,5 +1,4 @@ import path from "path" -import fs from "fs/promises" import z from "zod" import { Global } from "../global" @@ -65,16 +64,14 @@ export namespace McpAuth { if (serverUrl) { entry.serverUrl = serverUrl } - await Bun.write(file, JSON.stringify({ ...data, [mcpName]: entry }, null, 2)) - await fs.chmod(file.name!, 0o600) + await Bun.write(file, JSON.stringify({ ...data, [mcpName]: entry }, null, 2), { mode: 0o600 }) } export async function remove(mcpName: string): Promise { const file = Bun.file(filepath) const data = await all() delete data[mcpName] - await Bun.write(file, JSON.stringify(data, null, 2)) - await fs.chmod(file.name!, 0o600) + await Bun.write(file, JSON.stringify(data, null, 2), { mode: 0o600 }) } export async function updateTokens(mcpName: string, tokens: Tokens, serverUrl?: string): Promise { diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index 198e8ce25d..b6f1a96a9f 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -10,6 +10,7 @@ const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann" const ISSUER = "https://auth.openai.com" const CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses" const OAUTH_PORT = 1455 +const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000 interface PkceCodes { verifier: string @@ -461,7 +462,7 @@ export async function CodexAuthPlugin(input: PluginInput): Promise { }, methods: [ { - label: "ChatGPT Pro/Plus", + label: "ChatGPT Pro/Plus (browser)", type: "oauth", authorize: async () => { const { redirectUri } = await startOAuthServer() @@ -490,6 +491,89 @@ export async function CodexAuthPlugin(input: PluginInput): Promise { } }, }, + { + label: "ChatGPT Pro/Plus (headless)", + type: "oauth", + authorize: async () => { + const deviceResponse = await fetch(`${ISSUER}/api/accounts/deviceauth/usercode`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "User-Agent": `opencode/${Installation.VERSION}`, + }, + body: JSON.stringify({ client_id: CLIENT_ID }), + }) + + if (!deviceResponse.ok) throw new Error("Failed to initiate device authorization") + + const deviceData = (await deviceResponse.json()) as { + device_auth_id: string + user_code: string + interval: string + } + const interval = Math.max(parseInt(deviceData.interval) || 5, 1) * 1000 + + return { + url: `${ISSUER}/codex/device`, + instructions: `Enter code: ${deviceData.user_code}`, + method: "auto" as const, + async callback() { + while (true) { + const response = await fetch(`${ISSUER}/api/accounts/deviceauth/token`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "User-Agent": `opencode/${Installation.VERSION}`, + }, + body: JSON.stringify({ + device_auth_id: deviceData.device_auth_id, + user_code: deviceData.user_code, + }), + }) + + if (response.ok) { + const data = (await response.json()) as { + authorization_code: string + code_verifier: string + } + + const tokenResponse = await fetch(`${ISSUER}/oauth/token`, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: "authorization_code", + code: data.authorization_code, + redirect_uri: `${ISSUER}/deviceauth/callback`, + client_id: CLIENT_ID, + code_verifier: data.code_verifier, + }).toString(), + }) + + if (!tokenResponse.ok) { + throw new Error(`Token exchange failed: ${tokenResponse.status}`) + } + + const tokens: TokenResponse = await tokenResponse.json() + + return { + type: "success" as const, + refresh: tokens.refresh_token, + access: tokens.access_token, + expires: Date.now() + (tokens.expires_in ?? 3600) * 1000, + accountId: extractAccountId(tokens), + } + } + + if (response.status !== 403 && response.status !== 404) { + return { type: "failed" as const } + } + + await Bun.sleep(interval + OAUTH_POLLING_SAFETY_MARGIN_MS) + } + }, + } + }, + }, { label: "Manually enter API Key", type: "api", diff --git a/packages/opencode/src/plugin/copilot.ts b/packages/opencode/src/plugin/copilot.ts index 0be1345871..51f29db5ed 100644 --- a/packages/opencode/src/plugin/copilot.ts +++ b/packages/opencode/src/plugin/copilot.ts @@ -61,12 +61,13 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise { const info = await getAuth() if (info.type !== "oauth") return fetch(request, init) + const url = request instanceof URL ? request.href : request.toString() const { isVision, isAgent } = iife(() => { try { const body = typeof init?.body === "string" ? JSON.parse(init.body) : init?.body // Completions API - if (body?.messages) { + if (body?.messages && url.includes("completions")) { const last = body.messages[body.messages.length - 1] return { isVision: body.messages.some( @@ -88,6 +89,28 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise { isAgent: last?.role !== "user", } } + + // Messages API + if (body?.messages) { + const last = body.messages[body.messages.length - 1] + const hasNonToolCalls = + Array.isArray(last?.content) && last.content.some((part: any) => part?.type !== "tool_result") + return { + isVision: body.messages.some( + (item: any) => + Array.isArray(item?.content) && + item.content.some( + (part: any) => + part?.type === "image" || + // images can be nested inside tool_result content + (part?.type === "tool_result" && + Array.isArray(part?.content) && + part.content.some((nested: any) => nested?.type === "image")), + ), + ), + isAgent: !(last?.role === "user" && hasNonToolCalls), + } + } } catch {} return { isVision: false, isAgent: false } }) diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index 691fff4b2f..6032935f84 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -15,7 +15,7 @@ import { CopilotAuthPlugin } from "./copilot" export namespace Plugin { const log = Log.create({ service: "plugin" }) - const BUILTIN = ["opencode-anthropic-auth@0.0.10", "@gitlab/opencode-gitlab-auth@1.3.2"] + const BUILTIN = ["opencode-anthropic-auth@0.0.13", "@gitlab/opencode-gitlab-auth@1.3.2"] // Built-in plugins that are directly imported (not installed from npm) const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin] diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts index ddaa90f1e2..98031f18d3 100644 --- a/packages/opencode/src/project/instance.ts +++ b/packages/opencode/src/project/instance.ts @@ -14,6 +14,10 @@ interface Context { const context = Context.create("instance") const cache = new Map>() +const disposal = { + all: undefined as Promise | undefined, +} + export const Instance = { async provide(input: { directory: string; init?: () => Promise; fn: () => R }): Promise { let existing = cache.get(input.directory) @@ -77,15 +81,34 @@ export const Instance = { }) }, async disposeAll() { - Log.Default.info("disposing all instances") - for (const [_key, value] of cache) { - const awaited = await value.catch(() => {}) - if (awaited) { - await context.provide(await value, async () => { + if (disposal.all) return disposal.all + + disposal.all = iife(async () => { + Log.Default.info("disposing all instances") + const entries = [...cache.entries()] + for (const [key, value] of entries) { + if (cache.get(key) !== value) continue + + const ctx = await value.catch((error) => { + Log.Default.warn("instance dispose failed", { key, error }) + return undefined + }) + + if (!ctx) { + if (cache.get(key) === value) cache.delete(key) + continue + } + + if (cache.get(key) !== value) continue + + await context.provide(ctx, async () => { await Instance.dispose() }) } - } - cache.clear() + }).finally(() => { + disposal.all = undefined + }) + + return disposal.all }, } diff --git a/packages/opencode/src/project/state.ts b/packages/opencode/src/project/state.ts index 34a5dbb3e7..a9dce565b5 100644 --- a/packages/opencode/src/project/state.ts +++ b/packages/opencode/src/project/state.ts @@ -46,20 +46,24 @@ export namespace State { }, 10000).unref() const tasks: Promise[] = [] - for (const entry of entries.values()) { + for (const [init, entry] of entries) { if (!entry.dispose) continue + const label = typeof init === "function" ? init.name : String(init) + const task = Promise.resolve(entry.state) .then((state) => entry.dispose!(state)) .catch((error) => { - log.error("Error while disposing state:", { error, key }) + log.error("Error while disposing state:", { error, key, init: label }) }) tasks.push(task) } + await Promise.all(tasks) + entries.clear() recordsByKey.delete(key) - await Promise.all(tasks) + disposalFinished = true log.info("state disposal completed", { key }) } diff --git a/packages/opencode/src/server/routes/global.ts b/packages/opencode/src/server/routes/global.ts index c3c5ca5eba..5e2df052ec 100644 --- a/packages/opencode/src/server/routes/global.ts +++ b/packages/opencode/src/server/routes/global.ts @@ -1,5 +1,5 @@ import { Hono } from "hono" -import { describeRoute, resolver } from "hono-openapi" +import { describeRoute, resolver, validator } from "hono-openapi" import { streamSSE } from "hono/streaming" import z from "zod" import { BusEvent } from "@/bus/bus-event" @@ -8,6 +8,8 @@ import { Instance } from "../../project/instance" import { Installation } from "@/installation" import { Log } from "../../util/log" import { lazy } from "../../util/lazy" +import { Config } from "../../config/config" +import { errors } from "../error" const log = Log.create({ service: "server" }) @@ -103,6 +105,52 @@ export const GlobalRoutes = lazy(() => }) }, ) + .get( + "/config", + describeRoute({ + summary: "Get global configuration", + description: "Retrieve the current global OpenCode configuration settings and preferences.", + operationId: "global.config.get", + responses: { + 200: { + description: "Get global config info", + content: { + "application/json": { + schema: resolver(Config.Info), + }, + }, + }, + }, + }), + async (c) => { + return c.json(await Config.getGlobal()) + }, + ) + .patch( + "/config", + describeRoute({ + summary: "Update global configuration", + description: "Update global OpenCode configuration settings and preferences.", + operationId: "global.config.update", + responses: { + 200: { + description: "Successfully updated global config", + content: { + "application/json": { + schema: resolver(Config.Info), + }, + }, + }, + ...errors(400), + }, + }), + validator("json", Config.Info), + async (c) => { + const config = c.req.valid("json") + const next = await Config.updateGlobal(config) + return c.json(next) + }, + ) .post( "/dispose", describeRoute({ diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index cef24a066f..7d2023fc0e 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -122,6 +122,68 @@ export namespace Server { }), ) .route("/global", GlobalRoutes()) + .put( + "/auth/:providerID", + describeRoute({ + summary: "Set auth credentials", + description: "Set authentication credentials", + operationId: "auth.set", + responses: { + 200: { + description: "Successfully set authentication credentials", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + ...errors(400), + }, + }), + validator( + "param", + z.object({ + providerID: z.string(), + }), + ), + validator("json", Auth.Info), + async (c) => { + const providerID = c.req.valid("param").providerID + const info = c.req.valid("json") + await Auth.set(providerID, info) + return c.json(true) + }, + ) + .delete( + "/auth/:providerID", + describeRoute({ + summary: "Remove auth credentials", + description: "Remove authentication credentials", + operationId: "auth.remove", + responses: { + 200: { + description: "Successfully removed authentication credentials", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + ...errors(400), + }, + }), + validator( + "param", + z.object({ + providerID: z.string(), + }), + ), + async (c) => { + const providerID = c.req.valid("param").providerID + await Auth.remove(providerID) + return c.json(true) + }, + ) .use(async (c, next) => { let directory = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd() try { @@ -409,68 +471,6 @@ export namespace Server { return c.json(await Format.status()) }, ) - .put( - "/auth/:providerID", - describeRoute({ - summary: "Set auth credentials", - description: "Set authentication credentials", - operationId: "auth.set", - responses: { - 200: { - description: "Successfully set authentication credentials", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, - }, - }, - ...errors(400), - }, - }), - validator( - "param", - z.object({ - providerID: z.string(), - }), - ), - validator("json", Auth.Info), - async (c) => { - const providerID = c.req.valid("param").providerID - const info = c.req.valid("json") - await Auth.set(providerID, info) - return c.json(true) - }, - ) - .delete( - "/auth/:providerID", - describeRoute({ - summary: "Remove auth credentials", - description: "Remove authentication credentials", - operationId: "auth.remove", - responses: { - 200: { - description: "Successfully removed authentication credentials", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, - }, - }, - ...errors(400), - }, - }), - validator( - "param", - z.object({ - providerID: z.string(), - }), - ), - async (c) => { - const providerID = c.req.valid("param").providerID - await Auth.remove(providerID) - return c.json(true) - }, - ) .get( "/event", describeRoute({ diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index d413e80f69..723439a3fd 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -41,6 +41,32 @@ async function resolveRelative(instruction: string): Promise { } export namespace InstructionPrompt { + const state = Instance.state(() => { + return { + claims: new Map>(), + } + }) + + function isClaimed(messageID: string, filepath: string) { + const claimed = state().claims.get(messageID) + if (!claimed) return false + return claimed.has(filepath) + } + + function claim(messageID: string, filepath: string) { + const current = state() + let claimed = current.claims.get(messageID) + if (!claimed) { + claimed = new Set() + current.claims.set(messageID, claimed) + } + claimed.add(filepath) + } + + export function clear(messageID: string) { + state().claims.delete(messageID) + } + export async function systemPaths() { const config = await Config.get() const paths = new Set() @@ -137,7 +163,7 @@ export namespace InstructionPrompt { } } - export async function resolve(messages: MessageV2.WithParts[], filepath: string) { + export async function resolve(messages: MessageV2.WithParts[], filepath: string, messageID: string) { const system = await systemPaths() const already = loaded(messages) const results: { filepath: string; content: string }[] = [] @@ -147,7 +173,8 @@ export namespace InstructionPrompt { while (current.startsWith(root)) { const found = await find(current) - if (found && !system.has(found) && !already.has(found)) { + if (found && !system.has(found) && !already.has(found) && !isClaimed(messageID, found)) { + claim(messageID, found) const content = await Bun.file(found) .text() .catch(() => undefined) diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 020994bf1d..f5a4d7abbd 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -177,6 +177,8 @@ export namespace MessageV2 { }) .optional(), command: z.string().optional(), + }).meta({ + ref: "SubtaskPart", }) export type SubtaskPart = z.infer diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 04b110c9db..f050c43e97 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -549,6 +549,7 @@ export namespace SessionPrompt { model, abort, }) + using _ = defer(() => InstructionPrompt.clear(processor.message.id)) // Check if user explicitly invoked an agent via @ in this turn const lastUserMsg = msgs.findLast((m) => m.info.role === "user") @@ -837,6 +838,7 @@ export namespace SessionPrompt { system: input.system, variant: input.variant, } + using _ = defer(() => InstructionPrompt.clear(info.id)) const parts = await Promise.all( input.parts.map(async (part): Promise => { diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 746e0b173c..f230cdf44c 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -60,7 +60,7 @@ export const ReadTool = Tool.define("read", { throw new Error(`File not found: ${filepath}`) } - const instructions = await InstructionPrompt.resolve(ctx.messages, filepath) + const instructions = await InstructionPrompt.resolve(ctx.messages, filepath, ctx.messageID) // Exclude SVG (XML-based) and vnd.fastbidsheet (.fbs extension, commonly FlatBuffers schema files) const isImage = diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index c87add638a..ad4268b7b0 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -159,8 +159,10 @@ export const TaskTool = Tool.define("task", async (ctx) => { ...Object.fromEntries((config.experimental?.primary_tools ?? []).map((t) => [t, false])), }, parts: promptParts, + }).finally(() => { + unsub() }) - unsub() + const messages = await Session.messages({ sessionID: session.id }) const summary = messages .filter((x) => x.info.role === "assistant") diff --git a/packages/opencode/test/session/instruction.test.ts b/packages/opencode/test/session/instruction.test.ts index 2c44a266e0..67719fa339 100644 --- a/packages/opencode/test/session/instruction.test.ts +++ b/packages/opencode/test/session/instruction.test.ts @@ -18,7 +18,7 @@ describe("InstructionPrompt.resolve", () => { const system = await InstructionPrompt.systemPaths() expect(system.has(path.join(tmp.path, "AGENTS.md"))).toBe(true) - const results = await InstructionPrompt.resolve([], path.join(tmp.path, "src", "file.ts")) + const results = await InstructionPrompt.resolve([], path.join(tmp.path, "src", "file.ts"), "test-message-1") expect(results).toEqual([]) }, }) @@ -37,7 +37,11 @@ describe("InstructionPrompt.resolve", () => { const system = await InstructionPrompt.systemPaths() expect(system.has(path.join(tmp.path, "subdir", "AGENTS.md"))).toBe(false) - const results = await InstructionPrompt.resolve([], path.join(tmp.path, "subdir", "nested", "file.ts")) + const results = await InstructionPrompt.resolve( + [], + path.join(tmp.path, "subdir", "nested", "file.ts"), + "test-message-2", + ) expect(results.length).toBe(1) expect(results[0].filepath).toBe(path.join(tmp.path, "subdir", "AGENTS.md")) }, diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 7e8628de7a..574f17a7f4 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.1.36", + "version": "1.1.40", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index e6d968ed62..4b4faebab2 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.1.36", + "version": "1.1.40", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index d39dd2b348..b757b75350 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -14,7 +14,7 @@ import type { AuthSetErrors, AuthSetResponses, CommandListResponses, - Config as Config2, + Config as Config3, ConfigGetResponses, ConfigProvidersResponses, ConfigUpdateErrors, @@ -34,6 +34,9 @@ import type { FindSymbolsResponses, FindTextResponses, FormatterStatusResponses, + GlobalConfigGetResponses, + GlobalConfigUpdateErrors, + GlobalConfigUpdateResponses, GlobalDisposeResponses, GlobalEventResponses, GlobalHealthResponses, @@ -215,6 +218,44 @@ class HeyApiRegistry { } } +export class Config extends HeyApiClient { + /** + * Get global configuration + * + * Retrieve the current global OpenCode configuration settings and preferences. + */ + public get(options?: Options) { + return (options?.client ?? this.client).get({ + url: "/global/config", + ...options, + }) + } + + /** + * Update global configuration + * + * Update global OpenCode configuration settings and preferences. + */ + public update( + parameters?: { + config?: Config3 + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ key: "config", map: "body" }] }]) + return (options?.client ?? this.client).patch({ + url: "/global/config", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + export class Global extends HeyApiClient { /** * Get health @@ -251,6 +292,67 @@ export class Global extends HeyApiClient { ...options, }) } + + private _config?: Config + get config(): Config { + return (this._config ??= new Config({ client: this.client })) + } +} + +export class Auth extends HeyApiClient { + /** + * Remove auth credentials + * + * Remove authentication credentials + */ + public remove( + parameters: { + providerID: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "providerID" }] }]) + return (options?.client ?? this.client).delete({ + url: "/auth/{providerID}", + ...options, + ...params, + }) + } + + /** + * Set auth credentials + * + * Set authentication credentials + */ + public set( + parameters: { + providerID: string + auth?: Auth3 + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "providerID" }, + { key: "auth", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).put({ + url: "/auth/{providerID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } } export class Project extends HeyApiClient { @@ -541,7 +643,7 @@ export class Pty extends HeyApiClient { } } -export class Config extends HeyApiClient { +export class Config2 extends HeyApiClient { /** * Get configuration * @@ -569,7 +671,7 @@ export class Config extends HeyApiClient { public update( parameters?: { directory?: string - config?: Config2 + config?: Config3 }, options?: Options, ) { @@ -2238,7 +2340,7 @@ export class File extends HeyApiClient { } } -export class Auth extends HeyApiClient { +export class Auth2 extends HeyApiClient { /** * Remove MCP OAuth * @@ -2482,9 +2584,9 @@ export class Mcp extends HeyApiClient { }) } - private _auth?: Auth - get auth(): Auth { - return (this._auth ??= new Auth({ client: this.client })) + private _auth?: Auth2 + get auth(): Auth2 { + return (this._auth ??= new Auth2({ client: this.client })) } } @@ -3055,75 +3157,6 @@ export class Formatter extends HeyApiClient { } } -export class Auth2 extends HeyApiClient { - /** - * Remove auth credentials - * - * Remove authentication credentials - */ - public remove( - parameters: { - providerID: string - directory?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "providerID" }, - { in: "query", key: "directory" }, - ], - }, - ], - ) - return (options?.client ?? this.client).delete({ - url: "/auth/{providerID}", - ...options, - ...params, - }) - } - - /** - * Set auth credentials - * - * Set authentication credentials - */ - public set( - parameters: { - providerID: string - directory?: string - auth?: Auth3 - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "providerID" }, - { in: "query", key: "directory" }, - { key: "auth", map: "body" }, - ], - }, - ], - ) - return (options?.client ?? this.client).put({ - url: "/auth/{providerID}", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - export class Event extends HeyApiClient { /** * Subscribe to events @@ -3158,6 +3191,11 @@ export class OpencodeClient extends HeyApiClient { return (this._global ??= new Global({ client: this.client })) } + private _auth?: Auth + get auth(): Auth { + return (this._auth ??= new Auth({ client: this.client })) + } + private _project?: Project get project(): Project { return (this._project ??= new Project({ client: this.client })) @@ -3168,9 +3206,9 @@ export class OpencodeClient extends HeyApiClient { return (this._pty ??= new Pty({ client: this.client })) } - private _config?: Config - get config(): Config { - return (this._config ??= new Config({ client: this.client })) + private _config?: Config2 + get config(): Config2 { + return (this._config ??= new Config2({ client: this.client })) } private _tool?: Tool @@ -3268,11 +3306,6 @@ export class OpencodeClient extends HeyApiClient { return (this._formatter ??= new Formatter({ client: this.client })) } - private _auth?: Auth2 - get auth(): Auth2 { - return (this._auth ??= new Auth2({ client: this.client })) - } - private _event?: Event get event(): Event { return (this._event ??= new Event({ client: this.client })) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 2a63d72121..12c7bf7dfd 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -233,6 +233,21 @@ export type TextPart = { } } +export type SubtaskPart = { + id: string + sessionID: string + messageID: string + type: "subtask" + prompt: string + description: string + agent: string + model?: { + providerID: string + modelID: string + } + command?: string +} + export type ReasoningPart = { id: string sessionID: string @@ -449,20 +464,7 @@ export type CompactionPart = { export type Part = | TextPart - | { - id: string - sessionID: string - messageID: string - type: "subtask" - prompt: string - description: string - agent: string - model?: { - providerID: string - modelID: string - } - command?: string - } + | SubtaskPart | ReasoningPart | FilePart | ToolPart @@ -930,21 +932,6 @@ export type GlobalEvent = { payload: Event } -export type BadRequestError = { - data: unknown - errors: Array<{ - [key: string]: unknown - }> - success: false -} - -export type NotFoundError = { - name: "NotFoundError" - data: { - message: string - } -} - /** * Custom keybind configurations */ @@ -1826,6 +1813,43 @@ export type Config = { } } +export type BadRequestError = { + data: unknown + errors: Array<{ + [key: string]: unknown + }> + success: false +} + +export type OAuth = { + type: "oauth" + refresh: string + access: string + expires: number + accountId?: string + enterpriseUrl?: string +} + +export type ApiAuth = { + type: "api" + key: string +} + +export type WellKnownAuth = { + type: "wellknown" + key: string + token: string +} + +export type Auth = OAuth | ApiAuth | WellKnownAuth + +export type NotFoundError = { + name: "NotFoundError" + data: { + message: string + } +} + export type Model = { id: string providerID: string @@ -2142,28 +2166,6 @@ export type FormatterStatus = { enabled: boolean } -export type OAuth = { - type: "oauth" - refresh: string - access: string - expires: number - accountId?: string - enterpriseUrl?: string -} - -export type ApiAuth = { - type: "api" - key: string -} - -export type WellKnownAuth = { - type: "wellknown" - key: string - token: string -} - -export type Auth = OAuth | ApiAuth | WellKnownAuth - export type GlobalHealthData = { body?: never path?: never @@ -2199,6 +2201,47 @@ export type GlobalEventResponses = { export type GlobalEventResponse = GlobalEventResponses[keyof GlobalEventResponses] +export type GlobalConfigGetData = { + body?: never + path?: never + query?: never + url: "/global/config" +} + +export type GlobalConfigGetResponses = { + /** + * Get global config info + */ + 200: Config +} + +export type GlobalConfigGetResponse = GlobalConfigGetResponses[keyof GlobalConfigGetResponses] + +export type GlobalConfigUpdateData = { + body?: Config + path?: never + query?: never + url: "/global/config" +} + +export type GlobalConfigUpdateErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type GlobalConfigUpdateError = GlobalConfigUpdateErrors[keyof GlobalConfigUpdateErrors] + +export type GlobalConfigUpdateResponses = { + /** + * Successfully updated global config + */ + 200: Config +} + +export type GlobalConfigUpdateResponse = GlobalConfigUpdateResponses[keyof GlobalConfigUpdateResponses] + export type GlobalDisposeData = { body?: never path?: never @@ -2215,6 +2258,60 @@ export type GlobalDisposeResponses = { export type GlobalDisposeResponse = GlobalDisposeResponses[keyof GlobalDisposeResponses] +export type AuthRemoveData = { + body?: never + path: { + providerID: string + } + query?: never + url: "/auth/{providerID}" +} + +export type AuthRemoveErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type AuthRemoveError = AuthRemoveErrors[keyof AuthRemoveErrors] + +export type AuthRemoveResponses = { + /** + * Successfully removed authentication credentials + */ + 200: boolean +} + +export type AuthRemoveResponse = AuthRemoveResponses[keyof AuthRemoveResponses] + +export type AuthSetData = { + body?: Auth + path: { + providerID: string + } + query?: never + url: "/auth/{providerID}" +} + +export type AuthSetErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type AuthSetError = AuthSetErrors[keyof AuthSetErrors] + +export type AuthSetResponses = { + /** + * Successfully set authentication credentials + */ + 200: boolean +} + +export type AuthSetResponse = AuthSetResponses[keyof AuthSetResponses] + export type ProjectListData = { body?: never path?: never @@ -4867,64 +4964,6 @@ export type FormatterStatusResponses = { export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses] -export type AuthRemoveData = { - body?: never - path: { - providerID: string - } - query?: { - directory?: string - } - url: "/auth/{providerID}" -} - -export type AuthRemoveErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type AuthRemoveError = AuthRemoveErrors[keyof AuthRemoveErrors] - -export type AuthRemoveResponses = { - /** - * Successfully removed authentication credentials - */ - 200: boolean -} - -export type AuthRemoveResponse = AuthRemoveResponses[keyof AuthRemoveResponses] - -export type AuthSetData = { - body?: Auth - path: { - providerID: string - } - query?: { - directory?: string - } - url: "/auth/{providerID}" -} - -export type AuthSetErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type AuthSetError = AuthSetErrors[keyof AuthSetErrors] - -export type AuthSetResponses = { - /** - * Successfully set authentication credentials - */ - 200: boolean -} - -export type AuthSetResponse = AuthSetResponses[keyof AuthSetResponses] - export type EventSubscribeData = { body?: never path?: never diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index cf2f29d858..0c60bdd407 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -66,6 +66,73 @@ ] } }, + "/global/config": { + "get": { + "operationId": "global.config.get", + "summary": "Get global configuration", + "description": "Retrieve the current global OpenCode configuration settings and preferences.", + "responses": { + "200": { + "description": "Get global config info", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Config" + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.config.get({\n ...\n})" + } + ] + }, + "patch": { + "operationId": "global.config.update", + "summary": "Update global configuration", + "description": "Update global OpenCode configuration settings and preferences.", + "responses": { + "200": { + "description": "Successfully updated global config", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Config" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Config" + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.config.update({\n ...\n})" + } + ] + } + }, "/global/dispose": { "post": { "operationId": "global.dispose", @@ -91,6 +158,103 @@ ] } }, + "/auth/{providerID}": { + "put": { + "operationId": "auth.set", + "summary": "Set auth credentials", + "description": "Set authentication credentials", + "responses": { + "200": { + "description": "Successfully set authentication credentials", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "parameters": [ + { + "in": "path", + "name": "providerID", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Auth" + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.set({\n ...\n})" + } + ] + }, + "delete": { + "operationId": "auth.remove", + "summary": "Remove auth credentials", + "description": "Remove authentication credentials", + "responses": { + "200": { + "description": "Successfully removed authentication credentials", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "parameters": [ + { + "in": "path", + "name": "providerID", + "schema": { + "type": "string" + }, + "required": true + } + ], + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.remove({\n ...\n})" + } + ] + } + }, "/project": { "get": { "operationId": "project.list", @@ -5650,117 +5814,6 @@ ] } }, - "/auth/{providerID}": { - "put": { - "operationId": "auth.set", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "providerID", - "schema": { - "type": "string" - }, - "required": true - } - ], - "summary": "Set auth credentials", - "description": "Set authentication credentials", - "responses": { - "200": { - "description": "Successfully set authentication credentials", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Auth" - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.set({\n ...\n})" - } - ] - }, - "delete": { - "operationId": "auth.remove", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "providerID", - "schema": { - "type": "string" - }, - "required": true - } - ], - "summary": "Remove auth credentials", - "description": "Remove authentication credentials", - "responses": { - "200": { - "description": "Successfully removed authentication credentials", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.remove({\n ...\n})" - } - ] - } - }, "/event": { "get": { "operationId": "event.subscribe", @@ -6449,6 +6502,49 @@ }, "required": ["id", "sessionID", "messageID", "type", "text"] }, + "SubtaskPart": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "sessionID": { + "type": "string" + }, + "messageID": { + "type": "string" + }, + "type": { + "type": "string", + "const": "subtask" + }, + "prompt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "model": { + "type": "object", + "properties": { + "providerID": { + "type": "string" + }, + "modelID": { + "type": "string" + } + }, + "required": ["providerID", "modelID"] + }, + "command": { + "type": "string" + } + }, + "required": ["id", "sessionID", "messageID", "type", "prompt", "description", "agent"] + }, "ReasoningPart": { "type": "object", "properties": { @@ -7072,47 +7168,7 @@ "$ref": "#/components/schemas/TextPart" }, { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "sessionID": { - "type": "string" - }, - "messageID": { - "type": "string" - }, - "type": { - "type": "string", - "const": "subtask" - }, - "prompt": { - "type": "string" - }, - "description": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "modelID": { - "type": "string" - } - }, - "required": ["providerID", "modelID"] - }, - "command": { - "type": "string" - } - }, - "required": ["id", "sessionID", "messageID", "type", "prompt", "description", "agent"] + "$ref": "#/components/schemas/SubtaskPart" }, { "$ref": "#/components/schemas/ReasoningPart" @@ -8352,46 +8408,6 @@ }, "required": ["directory", "payload"] }, - "BadRequestError": { - "type": "object", - "properties": { - "data": {}, - "errors": { - "type": "array", - "items": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - }, - "success": { - "type": "boolean", - "const": false - } - }, - "required": ["data", "errors", "success"] - }, - "NotFoundError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "const": "NotFoundError" - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"] - } - }, - "required": ["name", "data"] - }, "KeybindsConfig": { "description": "Custom keybind configurations", "type": "object", @@ -9898,6 +9914,113 @@ }, "additionalProperties": false }, + "BadRequestError": { + "type": "object", + "properties": { + "data": {}, + "errors": { + "type": "array", + "items": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "success": { + "type": "boolean", + "const": false + } + }, + "required": ["data", "errors", "success"] + }, + "OAuth": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "oauth" + }, + "refresh": { + "type": "string" + }, + "access": { + "type": "string" + }, + "expires": { + "type": "number" + }, + "accountId": { + "type": "string" + }, + "enterpriseUrl": { + "type": "string" + } + }, + "required": ["type", "refresh", "access", "expires"] + }, + "ApiAuth": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "api" + }, + "key": { + "type": "string" + } + }, + "required": ["type", "key"] + }, + "WellKnownAuth": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "wellknown" + }, + "key": { + "type": "string" + }, + "token": { + "type": "string" + } + }, + "required": ["type", "key", "token"] + }, + "Auth": { + "anyOf": [ + { + "$ref": "#/components/schemas/OAuth" + }, + { + "$ref": "#/components/schemas/ApiAuth" + }, + { + "$ref": "#/components/schemas/WellKnownAuth" + } + ] + }, + "NotFoundError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "const": "NotFoundError" + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + }, + "required": ["name", "data"] + }, "Model": { "type": "object", "properties": { @@ -10824,73 +10947,6 @@ } }, "required": ["name", "extensions", "enabled"] - }, - "OAuth": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "oauth" - }, - "refresh": { - "type": "string" - }, - "access": { - "type": "string" - }, - "expires": { - "type": "number" - }, - "accountId": { - "type": "string" - }, - "enterpriseUrl": { - "type": "string" - } - }, - "required": ["type", "refresh", "access", "expires"] - }, - "ApiAuth": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "api" - }, - "key": { - "type": "string" - } - }, - "required": ["type", "key"] - }, - "WellKnownAuth": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "wellknown" - }, - "key": { - "type": "string" - }, - "token": { - "type": "string" - } - }, - "required": ["type", "key", "token"] - }, - "Auth": { - "anyOf": [ - { - "$ref": "#/components/schemas/OAuth" - }, - { - "$ref": "#/components/schemas/ApiAuth" - }, - { - "$ref": "#/components/schemas/WellKnownAuth" - } - ] } } } diff --git a/packages/slack/package.json b/packages/slack/package.json index 731c2d15d5..0ac4e9b07b 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.1.36", + "version": "1.1.40", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 4c663b1b1c..7cba4c66f9 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.1.36", + "version": "1.1.40", "type": "module", "license": "MIT", "exports": { diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx index 8aa9315e06..ce7704f37e 100644 --- a/packages/ui/src/components/dialog.tsx +++ b/packages/ui/src/components/dialog.tsx @@ -60,7 +60,9 @@ export function Dialog(props: DialogProps) {
- {props.description} + + {props.description} +
{props.children}
diff --git a/packages/ui/src/components/list.css b/packages/ui/src/components/list.css index c30d410f61..b12d304151 100644 --- a/packages/ui/src/components/list.css +++ b/packages/ui/src/components/list.css @@ -187,7 +187,7 @@ [data-slot="list-header"] { display: flex; z-index: 10; - padding: 8px 12px 8px 12px; + padding: 8px 12px 8px 8px; justify-content: space-between; align-items: center; align-self: stretch; diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 8b20a73b42..3f176db702 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -390,12 +390,14 @@ export function SessionTurn( const interval = Interval.fromDateTimes(from, to) const unit: DurationUnit[] = interval.length("seconds") > 60 ? ["minutes", "seconds"] : ["seconds"] - return interval.toDuration(unit).normalize().reconfigure({ locale: i18n.locale() }).toHuman({ + const locale = i18n.locale() + const human = interval.toDuration(unit).normalize().reconfigure({ locale }).toHuman({ notation: "compact", unitDisplay: "narrow", compactDisplay: "short", showZeros: false, }) + return locale.startsWith("zh") ? human.replaceAll("、", "") : human } const autoScroll = createAutoScroll({ diff --git a/packages/ui/src/context/marked.tsx b/packages/ui/src/context/marked.tsx index 71881353ae..0c6d58b935 100644 --- a/packages/ui/src/context/marked.tsx +++ b/packages/ui/src/context/marked.tsx @@ -475,6 +475,7 @@ export const { use: useMarked, provider: MarkedProvider } = createSimpleContext( }, markedKatex({ throwOnError: false, + nonStandard: true, }), markedShiki({ async highlight(code, lang) { diff --git a/packages/ui/src/i18n/th.ts b/packages/ui/src/i18n/th.ts new file mode 100644 index 0000000000..9c7e6fae51 --- /dev/null +++ b/packages/ui/src/i18n/th.ts @@ -0,0 +1,102 @@ +export const dict = { + "ui.sessionReview.title": "การเปลี่ยนแปลงเซสชัน", + "ui.sessionReview.diffStyle.unified": "แบบรวม", + "ui.sessionReview.diffStyle.split": "แบบแยก", + "ui.sessionReview.expandAll": "ขยายทั้งหมด", + "ui.sessionReview.collapseAll": "ย่อทั้งหมด", + "ui.sessionReview.change.added": "เพิ่ม", + "ui.sessionReview.change.removed": "ลบ", + + "ui.lineComment.label.prefix": "แสดงความคิดเห็นบน ", + "ui.lineComment.label.suffix": "", + "ui.lineComment.editorLabel.prefix": "กำลังแสดงความคิดเห็นบน ", + "ui.lineComment.editorLabel.suffix": "", + "ui.lineComment.placeholder": "เพิ่มความคิดเห็น", + "ui.lineComment.submit": "แสดงความคิดเห็น", + + "ui.sessionTurn.steps.show": "แสดงขั้นตอน", + "ui.sessionTurn.steps.hide": "ซ่อนขั้นตอน", + "ui.sessionTurn.summary.response": "การตอบสนอง", + "ui.sessionTurn.diff.showMore": "แสดงการเปลี่ยนแปลงเพิ่มเติม ({{count}})", + + "ui.sessionTurn.retry.retrying": "กำลังลองใหม่", + "ui.sessionTurn.retry.inSeconds": "ใน {{seconds}}วิ", + + "ui.sessionTurn.status.delegating": "มอบหมายงาน", + "ui.sessionTurn.status.planning": "วางแผนขั้นตอนถัดไป", + "ui.sessionTurn.status.gatheringContext": "รวบรวมบริบท", + "ui.sessionTurn.status.searchingCodebase": "กำลังค้นหาโค้ดเบส", + "ui.sessionTurn.status.searchingWeb": "กำลังค้นหาบนเว็บ", + "ui.sessionTurn.status.makingEdits": "กำลังแก้ไข", + "ui.sessionTurn.status.runningCommands": "กำลังเรียกใช้คำสั่ง", + "ui.sessionTurn.status.thinking": "กำลังคิด", + "ui.sessionTurn.status.thinkingWithTopic": "กำลังคิด - {{topic}}", + "ui.sessionTurn.status.gatheringThoughts": "รวบรวมความคิด", + "ui.sessionTurn.status.consideringNextSteps": "พิจารณาขั้นตอนถัดไป", + + "ui.messagePart.diagnostic.error": "ข้อผิดพลาด", + "ui.messagePart.title.edit": "แก้ไข", + "ui.messagePart.title.write": "เขียน", + "ui.messagePart.option.typeOwnAnswer": "พิมพ์คำตอบของคุณเอง", + "ui.messagePart.review.title": "ตรวจสอบคำตอบของคุณ", + + "ui.list.loading": "กำลังโหลด", + "ui.list.empty": "ไม่มีผลลัพธ์", + "ui.list.clearFilter": "ล้างตัวกรอง", + "ui.list.emptyWithFilter.prefix": "ไม่มีผลลัพธ์สำหรับ", + "ui.list.emptyWithFilter.suffix": "", + + "ui.messageNav.newMessage": "ข้อความใหม่", + + "ui.textField.copyToClipboard": "คัดลอกไปยังคลิปบอร์ด", + "ui.textField.copyLink": "คัดลอกลิงก์", + "ui.textField.copied": "คัดลอกแล้ว", + + "ui.imagePreview.alt": "ตัวอย่างรูปภาพ", + + "ui.tool.read": "อ่าน", + "ui.tool.list": "รายการ", + "ui.tool.glob": "Glob", + "ui.tool.grep": "Grep", + "ui.tool.webfetch": "ดึงจากเว็บ", + "ui.tool.shell": "เชลล์", + "ui.tool.patch": "แพตช์", + "ui.tool.todos": "รายการงาน", + "ui.tool.todos.read": "อ่านรายการงาน", + "ui.tool.questions": "คำถาม", + "ui.tool.agent": "เอเจนต์ {{type}}", + + "ui.common.file.one": "ไฟล์", + "ui.common.file.other": "ไฟล์", + "ui.common.question.one": "คำถาม", + "ui.common.question.other": "คำถาม", + + "ui.common.add": "เพิ่ม", + "ui.common.cancel": "ยกเลิก", + "ui.common.confirm": "ยืนยัน", + "ui.common.dismiss": "ปิด", + "ui.common.close": "ปิด", + "ui.common.next": "ถัดไป", + "ui.common.submit": "ส่ง", + + "ui.permission.deny": "ปฏิเสธ", + "ui.permission.allowAlways": "อนุญาตเสมอ", + "ui.permission.allowOnce": "อนุญาตครั้งเดียว", + + "ui.message.expand": "ขยายข้อความ", + "ui.message.collapse": "ย่อข้อความ", + "ui.message.copy": "คัดลอก", + "ui.message.copied": "คัดลอกแล้ว!", + "ui.message.attachment.alt": "ไฟล์แนบ", + + "ui.patch.action.deleted": "ลบ", + "ui.patch.action.created": "สร้าง", + "ui.patch.action.moved": "ย้าย", + "ui.patch.action.patched": "แพตช์", + + "ui.question.subtitle.answered": "{{count}} ตอบแล้ว", + "ui.question.answer.none": "(ไม่มีคำตอบ)", + "ui.question.review.notAnswered": "(ไม่ได้ตอบ)", + "ui.question.multiHint": "(เลือกทั้งหมดที่ใช้)", + "ui.question.custom.placeholder": "พิมพ์คำตอบของคุณ...", +} diff --git a/packages/ui/src/i18n/zh.ts b/packages/ui/src/i18n/zh.ts index 9a5a056a82..4ea477792b 100644 --- a/packages/ui/src/i18n/zh.ts +++ b/packages/ui/src/i18n/zh.ts @@ -20,7 +20,7 @@ export const dict = { "ui.sessionTurn.steps.show": "显示步骤", "ui.sessionTurn.steps.hide": "隐藏步骤", "ui.sessionTurn.summary.response": "回复", - "ui.sessionTurn.diff.showMore": "显示更多更改 ({{count}})", + "ui.sessionTurn.diff.showMore": "显示更多更改({{count}})", "ui.sessionTurn.retry.retrying": "重试中", "ui.sessionTurn.retry.inSeconds": "{{seconds}} 秒后", @@ -33,7 +33,7 @@ export const dict = { "ui.sessionTurn.status.makingEdits": "正在修改", "ui.sessionTurn.status.runningCommands": "正在运行命令", "ui.sessionTurn.status.thinking": "思考中", - "ui.sessionTurn.status.thinkingWithTopic": "思考 - {{topic}}", + "ui.sessionTurn.status.thinkingWithTopic": "思考:{{topic}}", "ui.sessionTurn.status.gatheringThoughts": "正在整理思路", "ui.sessionTurn.status.consideringNextSteps": "正在考虑下一步", diff --git a/packages/util/package.json b/packages/util/package.json index 6b9a8d26d5..f8360df7ed 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.1.36", + "version": "1.1.40", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index 12b3c68916..c3b8d0b750 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.1.36", + "version": "1.1.40", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/packages/web/src/content/docs/ecosystem.mdx b/packages/web/src/content/docs/ecosystem.mdx index fed64bf77b..07110dc1b5 100644 --- a/packages/web/src/content/docs/ecosystem.mdx +++ b/packages/web/src/content/docs/ecosystem.mdx @@ -15,37 +15,38 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw ## Plugins -| Name | Description | -| -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | -| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Automatically inject Helicone session headers for request grouping | -| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Auto-inject TypeScript/Svelte types into file reads with lookup tools | -| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Use your ChatGPT Plus/Pro subscription instead of API credits | -| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Use your existing Gemini plan instead of API billing | -| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Use Antigravity's free models instead of API billing | -| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Multi-branch devcontainer isolation with shallow clones and auto-assigned ports | -| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth Plugin, with support for Google Search, and more robust API handling | -| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimize token usage by pruning obsolete tool outputs | -| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Add native websearch support for supported providers with Google grounded style | -| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Enables AI agents to run background processes in a PTY, send interactive input to them. | -| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instructions for non-interactive shell commands - prevents hangs from TTY-dependent operations | -| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Track OpenCode usage with Wakatime | -| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Clean up markdown tables produced by LLMs | -| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x faster code editing with Morph Fast Apply API and lazy edit markers | -| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Background agents, pre-built LSP/AST/MCP tools, curated agents, Claude Code compatible | -| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Desktop notifications and sound alerts for OpenCode sessions | -| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Desktop notifications and sound alerts for permission, completion, and error events | -| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | AI-powered automatic Zellij session naming based on OpenCode context | -| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Allow OpenCode agents to lazy load prompts on demand with skill discovery and injection | -| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Persistent memory across sessions using Supermemory | -| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Interactive plan review with visual annotation and private/offline sharing | -| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Extend opencode /commands into a powerful orchestration system with granular flow control | -| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Schedule recurring jobs using launchd (Mac) or systemd (Linux) with cron syntax | -| [micode](https://github.com/vtemian/micode) | Structured Brainstorm → Plan → Implement workflow with session continuity | -| [octto](https://github.com/vtemian/octto) | Interactive browser UI for AI brainstorming with multi-question forms | -| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Claude Code-style background agents with async delegation and context persistence | -| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Native OS notifications for OpenCode – know when tasks complete | -| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Bundled multi-agent orchestration harness – 16 components, one install | -| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Zero-friction git worktrees for OpenCode | +| Name | Description | +| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| [opencode-daytona](https://github.com/jamesmurdza/daytona/tree/main/libs/opencode-plugin) | Automatically run OpenCode sessions in isolated Daytona sandboxes with git sync and live previews | +| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Automatically inject Helicone session headers for request grouping | +| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Auto-inject TypeScript/Svelte types into file reads with lookup tools | +| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Use your ChatGPT Plus/Pro subscription instead of API credits | +| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Use your existing Gemini plan instead of API billing | +| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Use Antigravity's free models instead of API billing | +| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Multi-branch devcontainer isolation with shallow clones and auto-assigned ports | +| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth Plugin, with support for Google Search, and more robust API handling | +| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimize token usage by pruning obsolete tool outputs | +| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Add native websearch support for supported providers with Google grounded style | +| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Enables AI agents to run background processes in a PTY, send interactive input to them. | +| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instructions for non-interactive shell commands - prevents hangs from TTY-dependent operations | +| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Track OpenCode usage with Wakatime | +| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Clean up markdown tables produced by LLMs | +| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x faster code editing with Morph Fast Apply API and lazy edit markers | +| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Background agents, pre-built LSP/AST/MCP tools, curated agents, Claude Code compatible | +| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Desktop notifications and sound alerts for OpenCode sessions | +| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Desktop notifications and sound alerts for permission, completion, and error events | +| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | AI-powered automatic Zellij session naming based on OpenCode context | +| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Allow OpenCode agents to lazy load prompts on demand with skill discovery and injection | +| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Persistent memory across sessions using Supermemory | +| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Interactive plan review with visual annotation and private/offline sharing | +| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Extend opencode /commands into a powerful orchestration system with granular flow control | +| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Schedule recurring jobs using launchd (Mac) or systemd (Linux) with cron syntax | +| [micode](https://github.com/vtemian/micode) | Structured Brainstorm → Plan → Implement workflow with session continuity | +| [octto](https://github.com/vtemian/octto) | Interactive browser UI for AI brainstorming with multi-question forms | +| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Claude Code-style background agents with async delegation and context persistence | +| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Native OS notifications for OpenCode – know when tasks complete | +| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Bundled multi-agent orchestration harness – 16 components, one install | +| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Zero-friction git worktrees for OpenCode | --- diff --git a/packages/web/src/content/docs/zen.mdx b/packages/web/src/content/docs/zen.mdx index 7a96838601..9fc732d057 100644 --- a/packages/web/src/content/docs/zen.mdx +++ b/packages/web/src/content/docs/zen.mdx @@ -116,7 +116,7 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**. | MiniMax M2.1 | $0.30 | $1.20 | $0.10 | - | | GLM 4.7 | $0.60 | $2.20 | $0.10 | - | | GLM 4.6 | $0.60 | $2.20 | $0.10 | - | -| Kimi K2.5 | $1.20 | $1.20 | $0.60 | - | +| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Kimi K2 Thinking | $0.40 | $2.50 | - | - | | Kimi K2 | $0.40 | $2.50 | - | - | | Qwen3 Coder 480B | $0.45 | $1.50 | - | - | diff --git a/script/publish-complete.ts b/script/publish-complete.ts index 4c7c7ac31c..a3bdceae07 100755 --- a/script/publish-complete.ts +++ b/script/publish-complete.ts @@ -1,11 +1,11 @@ #!/usr/bin/env bun -// import { Script } from "@opencode-ai/script" +import { Script } from "@opencode-ai/script" import { $ } from "bun" -// if (!Script.preview) { -// await $`gh release edit v${Script.version} --draft=false` -// } +if (!Script.preview) { + await $`gh release edit v${Script.version} --draft=false` +} await $`bun install` diff --git a/script/publish-start.ts b/script/publish-start.ts index 644790f9dc..385a2384bc 100755 --- a/script/publish-start.ts +++ b/script/publish-start.ts @@ -6,7 +6,7 @@ import { buildNotes, getLatestRelease } from "./changelog" const highlightsTemplate = `## Highlights -