diff --git a/packages/console/app/package.json b/packages/console/app/package.json
index d162f1ab0c..045363ea14 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.51",
+ "version": "1.1.52",
"type": "module",
"license": "MIT",
"scripts": {
diff --git a/packages/console/core/package.json b/packages/console/core/package.json
index d874adc18a..6db5462d8b 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.51",
+ "version": "1.1.52",
"private": true,
"type": "module",
"license": "MIT",
diff --git a/packages/console/function/package.json b/packages/console/function/package.json
index efe6dc4256..19e9da661f 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.51",
+ "version": "1.1.52",
"$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 9da4390c58..5e328cfb6f 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.51",
+ "version": "1.1.52",
"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 92676ec596..5991ace378 100644
--- a/packages/desktop/package.json
+++ b/packages/desktop/package.json
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
- "version": "1.1.51",
+ "version": "1.1.52",
"type": "module",
"license": "MIT",
"scripts": {
diff --git a/packages/desktop/src-tauri/capabilities/default.json b/packages/desktop/src-tauri/capabilities/default.json
index 66f068af8b..e895cdf78d 100644
--- a/packages/desktop/src-tauri/capabilities/default.json
+++ b/packages/desktop/src-tauri/capabilities/default.json
@@ -6,6 +6,19 @@
"permissions": [
"core:default",
"opener:default",
+ {
+ "identifier": "opener:allow-open-path",
+ "allow": [
+ { "path": "**/*" },
+ { "path": "/**/*" },
+ { "path": "**/.*/*/**" },
+ { "path": "/**/.*/*/**" },
+ { "path": "**/*", "app": true },
+ { "path": "/**/*", "app": true },
+ { "path": "**/.*/*/**", "app": true },
+ { "path": "/**/.*/*/**", "app": true }
+ ]
+ },
"deep-link:default",
"core:window:allow-start-dragging",
"core:window:allow-set-theme",
diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx
index b54e1f79f2..30cb7ba7ae 100644
--- a/packages/desktop/src/index.tsx
+++ b/packages/desktop/src/index.tsx
@@ -4,6 +4,7 @@ import { render } from "solid-js/web"
import { AppBaseProviders, AppInterface, PlatformProvider, Platform } from "@opencode-ai/app"
import { open, save } from "@tauri-apps/plugin-dialog"
import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link"
+import { openPath as openerOpenPath } from "@tauri-apps/plugin-opener"
import { open as shellOpen } from "@tauri-apps/plugin-shell"
import { type as ostype } from "@tauri-apps/plugin-os"
import { check, Update } from "@tauri-apps/plugin-updater"
@@ -87,6 +88,10 @@ const createPlatform = (password: Accessor
): Platform => ({
void shellOpen(url).catch(() => undefined)
},
+ openPath(path: string, app?: string) {
+ return openerOpenPath(path, app)
+ },
+
back() {
window.history.back()
},
diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json
index 13008747eb..0663f98ed0 100644
--- a/packages/enterprise/package.json
+++ b/packages/enterprise/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
- "version": "1.1.51",
+ "version": "1.1.52",
"private": true,
"type": "module",
"license": "MIT",
diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml
index eeadfa04f0..8df70b6434 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.51"
+version = "1.1.52"
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.51/opencode-darwin-arm64.zip"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.52/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.51/opencode-darwin-x64.zip"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.52/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
-archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.51/opencode-linux-arm64.tar.gz"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.52/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.51/opencode-linux-x64.tar.gz"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.52/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.51/opencode-windows-x64.zip"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.52/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
diff --git a/packages/function/package.json b/packages/function/package.json
index 110525c80f..e029196d4c 100644
--- a/packages/function/package.json
+++ b/packages/function/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
- "version": "1.1.51",
+ "version": "1.1.52",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index 8fe2ecae3c..b025acdc5d 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.51",
+ "version": "1.1.52",
"name": "opencode",
"type": "module",
"license": "MIT",
@@ -53,7 +53,7 @@
"dependencies": {
"@actions/core": "1.11.1",
"@actions/github": "6.0.1",
- "@agentclientprotocol/sdk": "0.13.0",
+ "@agentclientprotocol/sdk": "0.14.1",
"@ai-sdk/amazon-bedrock": "3.0.74",
"@ai-sdk/anthropic": "2.0.58",
"@ai-sdk/azure": "2.0.91",
diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts
index 2af375b996..c7b4fa922d 100644
--- a/packages/opencode/src/acp/agent.ts
+++ b/packages/opencode/src/acp/agent.ts
@@ -25,6 +25,7 @@ import {
type SetSessionModeResponse,
type ToolCallContent,
type ToolKind,
+ type Usage,
} from "@agentclientprotocol/sdk"
import { Log } from "../util/log"
@@ -38,7 +39,7 @@ import { Config } from "@/config/config"
import { Todo } from "@/session/todo"
import { z } from "zod"
import { LoadAPIKeyError } from "ai"
-import type { Event, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
+import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
import { applyPatch } from "diff"
type ModeOption = { id: string; name: string; description?: string }
@@ -49,6 +50,74 @@ const DEFAULT_VARIANT_VALUE = "default"
export namespace ACP {
const log = Log.create({ service: "acp-agent" })
+ async function getContextLimit(
+ sdk: OpencodeClient,
+ providerID: string,
+ modelID: string,
+ directory: string,
+ ): Promise {
+ const providers = await sdk.config
+ .providers({ directory })
+ .then((x) => x.data?.providers ?? [])
+ .catch((error) => {
+ log.error("failed to get providers for context limit", { error })
+ return []
+ })
+
+ const provider = providers.find((p) => p.id === providerID)
+ const model = provider?.models[modelID]
+ return model?.limit.context ?? null
+ }
+
+ async function sendUsageUpdate(
+ connection: AgentSideConnection,
+ sdk: OpencodeClient,
+ sessionID: string,
+ directory: string,
+ ): Promise {
+ const messages = await sdk.session
+ .messages({ sessionID, directory }, { throwOnError: true })
+ .then((x) => x.data)
+ .catch((error) => {
+ log.error("failed to fetch messages for usage update", { error })
+ return undefined
+ })
+
+ if (!messages) return
+
+ const assistantMessages = messages.filter(
+ (m): m is { info: AssistantMessage; parts: SessionMessageResponse["parts"] } => m.info.role === "assistant",
+ )
+
+ const lastAssistant = assistantMessages[assistantMessages.length - 1]
+ if (!lastAssistant) return
+
+ const msg = lastAssistant.info
+ const size = await getContextLimit(sdk, msg.providerID, msg.modelID, directory)
+
+ if (!size) {
+ // Cannot calculate usage without known context size
+ return
+ }
+
+ const used = msg.tokens.input + (msg.tokens.cache?.read ?? 0)
+ const totalCost = assistantMessages.reduce((sum, m) => sum + m.info.cost, 0)
+
+ await connection
+ .sessionUpdate({
+ sessionId: sessionID,
+ update: {
+ sessionUpdate: "usage_update",
+ used,
+ size,
+ cost: { amount: totalCost, currency: "USD" },
+ },
+ })
+ .catch((error) => {
+ log.error("failed to send usage update", { error })
+ })
+ }
+
export async function init({ sdk: _sdk }: { sdk: OpencodeClient }) {
return {
create: (connection: AgentSideConnection, fullConfig: ACPConfig) => {
@@ -568,6 +637,8 @@ export namespace ACP {
await this.processMessage(msg)
}
+ await sendUsageUpdate(this.connection, this.sdk, sessionId, directory)
+
return result
} catch (e) {
const error = MessageV2.fromError(e, {
@@ -676,6 +747,8 @@ export namespace ACP {
await this.processMessage(msg)
}
+ await sendUsageUpdate(this.connection, this.sdk, sessionId, directory)
+
return mode
} catch (e) {
const error = MessageV2.fromError(e, {
@@ -699,11 +772,15 @@ export namespace ACP {
log.info("resume_session", { sessionId, mcpServers: mcpServers.length })
- return this.loadSessionMode({
+ const result = await this.loadSessionMode({
cwd: directory,
mcpServers,
sessionId,
})
+
+ await sendUsageUpdate(this.connection, this.sdk, sessionId, directory)
+
+ return result
} catch (e) {
const error = MessageV2.fromError(e, {
providerID: this.config.defaultModel?.providerID ?? "unknown",
@@ -1261,13 +1338,22 @@ export namespace ACP {
return { name, args: rest.join(" ").trim() }
})()
- const done = {
- stopReason: "end_turn" as const,
- _meta: {},
- }
+ const buildUsage = (msg: AssistantMessage): Usage => ({
+ totalTokens:
+ msg.tokens.input +
+ msg.tokens.output +
+ msg.tokens.reasoning +
+ (msg.tokens.cache?.read ?? 0) +
+ (msg.tokens.cache?.write ?? 0),
+ inputTokens: msg.tokens.input,
+ outputTokens: msg.tokens.output,
+ thoughtTokens: msg.tokens.reasoning || undefined,
+ cachedReadTokens: msg.tokens.cache?.read || undefined,
+ cachedWriteTokens: msg.tokens.cache?.write || undefined,
+ })
if (!cmd) {
- await this.sdk.session.prompt({
+ const response = await this.sdk.session.prompt({
sessionID,
model: {
providerID: model.providerID,
@@ -1278,14 +1364,22 @@ export namespace ACP {
agent,
directory,
})
- return done
+ const msg = response.data?.info
+
+ await sendUsageUpdate(this.connection, this.sdk, sessionID, directory)
+
+ return {
+ stopReason: "end_turn" as const,
+ usage: msg ? buildUsage(msg) : undefined,
+ _meta: {},
+ }
}
const command = await this.config.sdk.command
.list({ directory }, { throwOnError: true })
.then((x) => x.data!.find((c) => c.name === cmd.name))
if (command) {
- await this.sdk.session.command({
+ const response = await this.sdk.session.command({
sessionID,
command: command.name,
arguments: cmd.args,
@@ -1293,7 +1387,15 @@ export namespace ACP {
agent,
directory,
})
- return done
+ const msg = response.data?.info
+
+ await sendUsageUpdate(this.connection, this.sdk, sessionID, directory)
+
+ return {
+ stopReason: "end_turn" as const,
+ usage: msg ? buildUsage(msg) : undefined,
+ _meta: {},
+ }
}
switch (cmd.name) {
@@ -1310,7 +1412,12 @@ export namespace ACP {
break
}
- return done
+ await sendUsageUpdate(this.connection, this.sdk, sessionID, directory)
+
+ return {
+ stopReason: "end_turn" as const,
+ _meta: {},
+ }
}
async cancel(params: CancelNotification) {
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
index dc3f337370..93e76cbdfd 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
@@ -154,7 +154,9 @@ function AutoMethod(props: AutoMethodProps) {
{props.title}
- esc
+ dialog.clear()}>
+ esc
+
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx
index c08fc99b6e..3e6e309514 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx
@@ -1,5 +1,6 @@
import { TextAttributes } from "@opentui/core"
import { useTheme } from "../context/theme"
+import { useDialog } from "@tui/ui/dialog"
import { useSync } from "@tui/context/sync"
import { For, Match, Switch, Show, createMemo } from "solid-js"
import { Installation } from "@/installation"
@@ -9,6 +10,7 @@ export type DialogStatusProps = {}
export function DialogStatus() {
const sync = useSync()
const { theme } = useTheme()
+ const dialog = useDialog()
const enabledFormatters = createMemo(() => sync.data.formatter.filter((f) => f.enabled))
@@ -43,7 +45,9 @@ export function DialogStatus() {
Status
- esc
+ dialog.clear()}>
+ esc
+
OpenCode v{Installation.VERSION}
0} fallback={No MCP Servers}>
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 e44ceb2889..4440466777 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -1625,6 +1625,7 @@ function BlockTool(props: {
function Bash(props: ToolProps) {
const { theme } = useTheme()
const sync = useSync()
+ const isRunning = createMemo(() => props.part.state.status === "running")
const output = createMemo(() => stripAnsi(props.metadata.output?.trim() ?? ""))
const [expanded, setExpanded] = createSignal(false)
const lines = createMemo(() => output().split("\n"))
@@ -1665,6 +1666,7 @@ function Bash(props: ToolProps) {
setExpanded((prev) => !prev) : undefined}
>
diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx
index 45e946fa7c..642c73b485 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx
+++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx
@@ -25,7 +25,9 @@ export function DialogAlert(props: DialogAlertProps) {
{props.title}
- esc
+ dialog.clear()}>
+ esc
+
{props.message}
diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx
index 8431a39461..b86bd43251 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx
+++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx
@@ -37,7 +37,9 @@ export function DialogConfirm(props: DialogConfirmProps) {
{props.title}
- esc
+ dialog.clear()}>
+ esc
+
{props.message}
diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx
index 867ed68100..1e8d09bb0b 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx
+++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx
@@ -80,7 +80,9 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
Export Options
- esc
+ dialog.clear()}>
+ esc
+
diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-help.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-help.tsx
index 056ce41dac..4e45279303 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/dialog-help.tsx
+++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-help.tsx
@@ -21,7 +21,9 @@ export function DialogHelp() {
Help
- esc/enter
+ dialog.clear()}>
+ esc/enter
+
diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx
index b296524124..b1b05a0f1a 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx
+++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx
@@ -39,7 +39,9 @@ export function DialogPrompt(props: DialogPromptProps) {
{props.title}
- esc
+ dialog.clear()}>
+ esc
+
{props.description}
diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
index 56d8453c93..490a100721 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
+++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
@@ -226,7 +226,9 @@ export function DialogSelect(props: DialogSelectProps) {
{props.title}
- esc
+ dialog.clear()}>
+ esc
+
{
"gpt-5.1-codex-mini",
"gpt-5.2",
"gpt-5.2-codex",
+ "gpt-5.3-codex",
"gpt-5.1-codex",
])
for (const modelId of Object.keys(provider.models)) {
@@ -369,6 +371,38 @@ export async function CodexAuthPlugin(input: PluginInput): Promise {
}
}
+ if (!provider.models["gpt-5.3-codex"]) {
+ const model = {
+ id: "gpt-5.3-codex",
+ providerID: "openai",
+ api: {
+ id: "gpt-5.3-codex",
+ url: "https://chatgpt.com/backend-api/codex",
+ npm: "@ai-sdk/openai",
+ },
+ name: "GPT-5.3 Codex",
+ capabilities: {
+ temperature: false,
+ reasoning: true,
+ attachment: true,
+ toolcall: true,
+ input: { text: true, audio: false, image: true, video: false, pdf: false },
+ output: { text: true, audio: false, image: false, video: false, pdf: false },
+ interleaved: false,
+ },
+ cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
+ limit: { context: 400_000, input: 272_000, output: 128_000 },
+ status: "active" as const,
+ options: {},
+ headers: {},
+ release_date: "2026-02-05",
+ variants: {} as Record>,
+ family: "gpt-codex",
+ }
+ model.variants = ProviderTransform.variants(model)
+ provider.models["gpt-5.3-codex"] = model
+ }
+
// Zero out costs for Codex (included with ChatGPT subscription)
for (const model of Object.values(provider.models)) {
model.cost = {
diff --git a/packages/opencode/src/plugin/copilot.ts b/packages/opencode/src/plugin/copilot.ts
index ef41ee38d3..39ea0d00d2 100644
--- a/packages/opencode/src/plugin/copilot.ts
+++ b/packages/opencode/src/plugin/copilot.ts
@@ -301,17 +301,20 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise {
},
],
},
- "chat.headers": async (input, output) => {
- if (!input.model.providerID.includes("github-copilot")) return
+ "chat.headers": async (incoming, output) => {
+ if (!incoming.model.providerID.includes("github-copilot")) return
- if (input.model.api.npm === "@ai-sdk/anthropic") {
+ if (incoming.model.api.npm === "@ai-sdk/anthropic") {
output.headers["anthropic-beta"] = "interleaved-thinking-2025-05-14"
}
const session = await sdk.session
.get({
path: {
- id: input.sessionID,
+ id: incoming.sessionID,
+ },
+ query: {
+ directory: input.directory,
},
throwOnError: true,
})
diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts
index 627c6f8cf0..fe8a905137 100644
--- a/packages/opencode/src/plugin/index.ts
+++ b/packages/opencode/src/plugin/index.ts
@@ -6,13 +6,18 @@ import { createOpencodeClient } from "@opencode-ai/sdk"
import { Server } from "../server/server"
import { BunProc } from "../bun"
import { Instance } from "../project/instance"
+import { Flag } from "../flag/flag"
import { CodexAuthPlugin } from "./codex"
+import { Session } from "../session"
+import { NamedError } from "@opencode-ai/util/error"
import { CopilotAuthPlugin } from "./copilot"
import { gitlabAuthPlugin as GitlabAuthPlugin } from "@gitlab/opencode-gitlab-auth"
export namespace Plugin {
const log = Log.create({ service: "plugin" })
+ const BUILTIN = ["opencode-anthropic-auth@0.0.13"]
+
// Built-in plugins that are directly imported (not installed from npm)
const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin, GitlabAuthPlugin]
@@ -39,8 +44,11 @@ export namespace Plugin {
hooks.push(init)
}
- const plugins = [...(config.plugin ?? [])]
+ let plugins = config.plugin ?? []
if (plugins.length) await Config.waitForDependencies()
+ if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
+ plugins = [...BUILTIN, ...plugins]
+ }
for (let plugin of plugins) {
// ignore old codex plugin since it is supported first party now
@@ -50,7 +58,24 @@ export namespace Plugin {
const lastAtIndex = plugin.lastIndexOf("@")
const pkg = lastAtIndex > 0 ? plugin.substring(0, lastAtIndex) : plugin
const version = lastAtIndex > 0 ? plugin.substring(lastAtIndex + 1) : "latest"
- plugin = await BunProc.install(pkg, version)
+ const builtin = BUILTIN.some((x) => x.startsWith(pkg + "@"))
+ plugin = await BunProc.install(pkg, version).catch((err) => {
+ if (!builtin) throw err
+
+ const message = err instanceof Error ? err.message : String(err)
+ log.error("failed to install builtin plugin", {
+ pkg,
+ version,
+ error: message,
+ })
+ Bus.publish(Session.Event.Error, {
+ error: new NamedError.Unknown({
+ message: `Failed to install built-in plugin ${pkg}@${version}: ${message}`,
+ }).toObject(),
+ })
+
+ return ""
+ })
if (!plugin) continue
}
const mod = await import(plugin)
diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts
index c1846af700..e564c54a1e 100644
--- a/packages/opencode/src/provider/transform.ts
+++ b/packages/opencode/src/provider/transform.ts
@@ -375,7 +375,8 @@ export namespace ProviderTransform {
}
}
const copilotEfforts = iife(() => {
- if (id.includes("5.1-codex-max") || id.includes("5.2")) return [...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
+ if (id.includes("5.1-codex-max") || id.includes("5.2") || id.includes("5.3"))
+ return [...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
return WIDELY_SUPPORTED_EFFORTS
})
return Object.fromEntries(
@@ -422,7 +423,7 @@ export namespace ProviderTransform {
if (id === "gpt-5-pro") return {}
const openaiEfforts = iife(() => {
if (id.includes("codex")) {
- if (id.includes("5.2")) return [...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
+ if (id.includes("5.2") || id.includes("5.3")) return [...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
return WIDELY_SUPPORTED_EFFORTS
}
const arr = [...WIDELY_SUPPORTED_EFFORTS]
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index 39b63ffe00..5eb7cedb0c 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -71,9 +71,6 @@ export namespace SessionPrompt {
async (current) => {
for (const item of Object.values(current)) {
item.abort.abort()
- for (const callback of item.callbacks) {
- callback.reject(new DOMException("Aborted", "AbortError"))
- }
}
},
)
@@ -249,9 +246,6 @@ export namespace SessionPrompt {
return
}
match.abort.abort()
- for (const item of match.callbacks) {
- item.reject(new DOMException("Aborted", "AbortError"))
- }
delete s[sessionID]
SessionStatus.set(sessionID, { type: "idle" })
return
diff --git a/packages/plugin/package.json b/packages/plugin/package.json
index d8b8733cb3..86275fc775 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.51",
+ "version": "1.1.52",
"type": "module",
"license": "MIT",
"scripts": {
diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json
index 19f4401a64..bda7bbe866 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.51",
+ "version": "1.1.52",
"type": "module",
"license": "MIT",
"scripts": {
diff --git a/packages/slack/package.json b/packages/slack/package.json
index 0c97a26c55..9522f33a20 100644
--- a/packages/slack/package.json
+++ b/packages/slack/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
- "version": "1.1.51",
+ "version": "1.1.52",
"type": "module",
"license": "MIT",
"scripts": {
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 83902a10f9..cdc8baa0b7 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
- "version": "1.1.51",
+ "version": "1.1.52",
"type": "module",
"license": "MIT",
"exports": {
@@ -18,6 +18,7 @@
"./theme/context": "./src/theme/context.tsx",
"./icons/provider": "./src/components/provider-icons/types.ts",
"./icons/file-type": "./src/components/file-icons/types.ts",
+ "./icons/app": "./src/components/app-icons/types.ts",
"./fonts/*": "./src/assets/fonts/*",
"./audio/*": "./src/assets/audio/*"
},
diff --git a/packages/ui/src/assets/icons/app/android-studio.svg b/packages/ui/src/assets/icons/app/android-studio.svg
new file mode 100644
index 0000000000..6b545e27a3
--- /dev/null
+++ b/packages/ui/src/assets/icons/app/android-studio.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/ui/src/assets/icons/app/antigravity.svg b/packages/ui/src/assets/icons/app/antigravity.svg
new file mode 100644
index 0000000000..3c3554af75
--- /dev/null
+++ b/packages/ui/src/assets/icons/app/antigravity.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/ui/src/assets/icons/app/cursor.svg b/packages/ui/src/assets/icons/app/cursor.svg
new file mode 100644
index 0000000000..c2c8c18199
--- /dev/null
+++ b/packages/ui/src/assets/icons/app/cursor.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/ui/src/assets/icons/app/finder.png b/packages/ui/src/assets/icons/app/finder.png
new file mode 100644
index 0000000000..4edf53bca9
Binary files /dev/null and b/packages/ui/src/assets/icons/app/finder.png differ
diff --git a/packages/ui/src/assets/icons/app/ghostty.svg b/packages/ui/src/assets/icons/app/ghostty.svg
new file mode 100644
index 0000000000..1dc652aac5
--- /dev/null
+++ b/packages/ui/src/assets/icons/app/ghostty.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/ui/src/assets/icons/app/iterm2.svg b/packages/ui/src/assets/icons/app/iterm2.svg
new file mode 100644
index 0000000000..0b00a1b7a7
--- /dev/null
+++ b/packages/ui/src/assets/icons/app/iterm2.svg
@@ -0,0 +1,25 @@
+
diff --git a/packages/ui/src/assets/icons/app/powershell.svg b/packages/ui/src/assets/icons/app/powershell.svg
new file mode 100644
index 0000000000..fa0c70f0b6
--- /dev/null
+++ b/packages/ui/src/assets/icons/app/powershell.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/ui/src/assets/icons/app/terminal.png b/packages/ui/src/assets/icons/app/terminal.png
new file mode 100644
index 0000000000..43857b6323
Binary files /dev/null and b/packages/ui/src/assets/icons/app/terminal.png differ
diff --git a/packages/ui/src/assets/icons/app/textmate.png b/packages/ui/src/assets/icons/app/textmate.png
new file mode 100644
index 0000000000..1eee73c5eb
Binary files /dev/null and b/packages/ui/src/assets/icons/app/textmate.png differ
diff --git a/packages/ui/src/assets/icons/app/vscode.svg b/packages/ui/src/assets/icons/app/vscode.svg
new file mode 100644
index 0000000000..aba7e19f6b
--- /dev/null
+++ b/packages/ui/src/assets/icons/app/vscode.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/ui/src/assets/icons/app/xcode.png b/packages/ui/src/assets/icons/app/xcode.png
new file mode 100644
index 0000000000..c37d9f1762
Binary files /dev/null and b/packages/ui/src/assets/icons/app/xcode.png differ
diff --git a/packages/ui/src/assets/icons/app/zed.svg b/packages/ui/src/assets/icons/app/zed.svg
new file mode 100644
index 0000000000..7c9a0e5914
--- /dev/null
+++ b/packages/ui/src/assets/icons/app/zed.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/ui/src/components/app-icon.css b/packages/ui/src/components/app-icon.css
new file mode 100644
index 0000000000..edcdbcceb5
--- /dev/null
+++ b/packages/ui/src/components/app-icon.css
@@ -0,0 +1,9 @@
+img[data-component="app-icon"] {
+ display: block;
+ box-sizing: border-box;
+ padding: 2px;
+ border-radius: 0.125rem;
+ background: var(--smoke-light-2);
+ border: 1px solid var(--smoke-light-alpha-4);
+ object-fit: contain;
+}
diff --git a/packages/ui/src/components/app-icon.tsx b/packages/ui/src/components/app-icon.tsx
new file mode 100644
index 0000000000..f58b5d38ce
--- /dev/null
+++ b/packages/ui/src/components/app-icon.tsx
@@ -0,0 +1,52 @@
+import type { Component, ComponentProps } from "solid-js"
+import { splitProps } from "solid-js"
+import type { IconName } from "./app-icons/types"
+
+import androidStudio from "../assets/icons/app/android-studio.svg"
+import antigravity from "../assets/icons/app/antigravity.svg"
+import cursor from "../assets/icons/app/cursor.svg"
+import finder from "../assets/icons/app/finder.png"
+import ghostty from "../assets/icons/app/ghostty.svg"
+import iterm2 from "../assets/icons/app/iterm2.svg"
+import powershell from "../assets/icons/app/powershell.svg"
+import terminal from "../assets/icons/app/terminal.png"
+import textmate from "../assets/icons/app/textmate.png"
+import vscode from "../assets/icons/app/vscode.svg"
+import xcode from "../assets/icons/app/xcode.png"
+import zed from "../assets/icons/app/zed.svg"
+
+const icons = {
+ vscode,
+ cursor,
+ zed,
+ finder,
+ terminal,
+ iterm2,
+ ghostty,
+ xcode,
+ "android-studio": androidStudio,
+ antigravity,
+ textmate,
+ powershell,
+} satisfies Record
+
+export type AppIconProps = Omit, "src"> & {
+ id: IconName
+}
+
+export const AppIcon: Component = (props) => {
+ const [local, rest] = splitProps(props, ["id", "class", "classList", "alt", "draggable"])
+ return (
+
+ )
+}
diff --git a/packages/ui/src/components/app-icons/sprite.svg b/packages/ui/src/components/app-icons/sprite.svg
new file mode 100644
index 0000000000..68361f4137
--- /dev/null
+++ b/packages/ui/src/components/app-icons/sprite.svg
@@ -0,0 +1,114 @@
+
diff --git a/packages/ui/src/components/app-icons/types.ts b/packages/ui/src/components/app-icons/types.ts
new file mode 100644
index 0000000000..81964b8dac
--- /dev/null
+++ b/packages/ui/src/components/app-icons/types.ts
@@ -0,0 +1,18 @@
+// This file is generated by icon spritesheet generator
+
+export const iconNames = [
+ "vscode",
+ "cursor",
+ "zed",
+ "finder",
+ "terminal",
+ "iterm2",
+ "ghostty",
+ "xcode",
+ "android-studio",
+ "antigravity",
+ "textmate",
+ "powershell",
+] as const
+
+export type IconName = (typeof iconNames)[number]
diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx
index 5c4678701b..5ea9f64bbc 100644
--- a/packages/ui/src/components/session-turn.tsx
+++ b/packages/ui/src/components/session-turn.tsx
@@ -10,7 +10,6 @@ import {
} from "@opencode-ai/sdk/v2/client"
import { useData } from "../context"
import { type UiI18nKey, type UiI18nParams, useI18n } from "../context/i18n"
-import { findLast } from "@opencode-ai/util/array"
import { Binary } from "@opencode-ai/util/binary"
import { createEffect, createMemo, createSignal, For, Match, on, onCleanup, ParentProps, Show, Switch } from "solid-js"
@@ -84,6 +83,7 @@ function AssistantMessageItem(props: {
responsePartId: string | undefined
hideResponsePart: boolean
hideReasoning: boolean
+ hidden?: () => readonly { messageID: string; callID: string }[]
}) {
const data = useData()
const emptyParts: PartType[] = []
@@ -104,13 +104,22 @@ function AssistantMessageItem(props: {
parts = parts.filter((part) => part?.type !== "reasoning")
}
- if (!props.hideResponsePart) return parts
+ if (props.hideResponsePart) {
+ const responsePartId = props.responsePartId
+ if (responsePartId && responsePartId === lastTextPart()?.id) {
+ parts = parts.filter((part) => part?.id !== responsePartId)
+ }
+ }
- const responsePartId = props.responsePartId
- if (!responsePartId) return parts
- if (responsePartId !== lastTextPart()?.id) return parts
+ const hidden = props.hidden?.() ?? []
+ if (hidden.length === 0) return parts
- return parts.filter((part) => part?.id !== responsePartId)
+ const id = props.message.id
+ return parts.filter((part) => {
+ if (part?.type !== "tool") return true
+ const tool = part as ToolPart
+ return !hidden.some((h) => h.messageID === id && h.callID === tool.callID)
+ })
})
return
@@ -140,7 +149,6 @@ export function SessionTurn(
const emptyFiles: FilePart[] = []
const emptyAssistant: AssistantMessage[] = []
const emptyPermissions: PermissionRequest[] = []
- const emptyPermissionParts: { part: ToolPart; message: AssistantMessage }[] = []
const emptyQuestions: QuestionRequest[] = []
const emptyQuestionParts: { part: ToolPart; message: AssistantMessage }[] = []
const idle = { type: "idle" as const }
@@ -253,48 +261,18 @@ export function SessionTurn(
})
const permissions = createMemo(() => data.store.permission?.[props.sessionID] ?? emptyPermissions)
- const permissionCount = createMemo(() => permissions().length)
const nextPermission = createMemo(() => permissions()[0])
- const permissionParts = createMemo(() => {
- if (props.stepsExpanded) return emptyPermissionParts
-
- const next = nextPermission()
- if (!next || !next.tool) return emptyPermissionParts
-
- const message = findLast(assistantMessages(), (m) => m.id === next.tool!.messageID)
- if (!message) return emptyPermissionParts
-
- const parts = data.store.part[message.id] ?? emptyParts
- for (const part of parts) {
- if (part?.type !== "tool") continue
- const tool = part as ToolPart
- if (tool.callID === next.tool?.callID) return [{ part: tool, message }]
- }
-
- return emptyPermissionParts
- })
-
const questions = createMemo(() => data.store.question?.[props.sessionID] ?? emptyQuestions)
const nextQuestion = createMemo(() => questions()[0])
- const questionParts = createMemo(() => {
- if (props.stepsExpanded) return emptyQuestionParts
-
- const next = nextQuestion()
- if (!next || !next.tool) return emptyQuestionParts
-
- const message = findLast(assistantMessages(), (m) => m.id === next.tool!.messageID)
- if (!message) return emptyQuestionParts
-
- const parts = data.store.part[message.id] ?? emptyParts
- for (const part of parts) {
- if (part?.type !== "tool") continue
- const tool = part as ToolPart
- if (tool.callID === next.tool?.callID) return [{ part: tool, message }]
- }
-
- return emptyQuestionParts
+ const hidden = createMemo(() => {
+ const out: { messageID: string; callID: string }[] = []
+ const perm = nextPermission()
+ if (perm?.tool) out.push(perm.tool)
+ const question = nextQuestion()
+ if (question?.tool) out.push(question.tool)
+ return out
})
const answeredQuestionParts = createMemo(() => {
@@ -499,14 +477,6 @@ export function SessionTurn(
onCleanup(() => clearInterval(timer))
})
- createEffect(
- on(permissionCount, (count, prev) => {
- if (!count) return
- if (prev !== undefined && count <= prev) return
- autoScroll.forceScrollToBottom()
- }),
- )
-
let lastStatusChange = Date.now()
let statusTimeout: number | undefined
createEffect(() => {
@@ -664,6 +634,7 @@ export function SessionTurn(
responsePartId={responsePartId()}
hideResponsePart={hideResponsePart()}
hideReasoning={!working()}
+ hidden={hidden}
/>
)}
@@ -674,20 +645,6 @@ export function SessionTurn(