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