diff --git a/.github/TEAM_MEMBERS b/.github/TEAM_MEMBERS
index 5268ff59dd..866d466a8b 100644
--- a/.github/TEAM_MEMBERS
+++ b/.github/TEAM_MEMBERS
@@ -20,3 +20,4 @@ Slickstef11
usrnk1
vimtor
StarpTech
+vaprdev
diff --git a/bun.lock b/bun.lock
index a8774d3aa2..338f7f0234 100644
--- a/bun.lock
+++ b/bun.lock
@@ -29,7 +29,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0",
@@ -96,7 +96,7 @@
},
"packages/cli": {
"name": "@opencode-ai/cli",
- "version": "1.18.30",
+ "version": "1.18.31",
"bin": {
"lildax": "./bin/lildax.cjs",
},
@@ -144,7 +144,7 @@
},
"packages/codemode": {
"name": "@opencode-ai/codemode",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"acorn": "8.15.0",
"effect": "catalog:",
@@ -158,7 +158,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -195,7 +195,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -222,7 +222,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"@ai-sdk/anthropic": "3.0.82",
"@ai-sdk/openai": "3.0.48",
@@ -245,7 +245,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -269,7 +269,7 @@
},
"packages/console/support": {
"name": "@opencode-ai/console-support",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@opencode-ai/console-core": "workspace:*",
@@ -289,7 +289,7 @@
},
"packages/core": {
"name": "@opencode-ai/core",
- "version": "1.18.30",
+ "version": "1.18.31",
"bin": {
"opencode": "./bin/opencode",
},
@@ -383,7 +383,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"@zip.js/zip.js": "2.7.62",
"drizzle-orm": "catalog:",
@@ -437,7 +437,7 @@
},
"packages/effect-drizzle-sqlite": {
"name": "@opencode-ai/effect-drizzle-sqlite",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"drizzle-orm": "catalog:",
"effect": "catalog:",
@@ -451,7 +451,7 @@
},
"packages/effect-sqlite-node": {
"name": "@opencode-ai/effect-sqlite-node",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"effect": "catalog:",
},
@@ -463,7 +463,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"@hono/standard-validator": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -495,7 +495,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -511,7 +511,7 @@
},
"packages/http-recorder": {
"name": "@opencode-ai/http-recorder",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"@effect/platform-node": "4.0.0-beta.83",
"@effect/platform-node-shared": "4.0.0-beta.83",
@@ -542,7 +542,7 @@
},
"packages/llm": {
"name": "@opencode-ai/llm",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"@opencode-ai/schema": "workspace:*",
"@smithy/eventstream-codec": "4.2.14",
@@ -561,7 +561,7 @@
},
"packages/opencode": {
"name": "opencode",
- "version": "1.18.30",
+ "version": "1.18.31",
"bin": {
"opencode": "./bin/opencode",
},
@@ -692,7 +692,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@opencode-ai/sdk": "workspace:*",
@@ -768,7 +768,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -783,7 +783,7 @@
},
"packages/server": {
"name": "@opencode-ai/server",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/protocol": "workspace:*",
@@ -798,7 +798,7 @@
},
"packages/session-ui": {
"name": "@opencode-ai/session-ui",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz",
@@ -838,7 +838,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -851,7 +851,7 @@
},
"packages/stats/app": {
"name": "@opencode-ai/stats-app",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"@ibm/plex": "6.4.1",
"@kobalte/core": "catalog:",
@@ -878,7 +878,7 @@
},
"packages/stats/core": {
"name": "@opencode-ai/stats-core",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"@aws-sdk/client-athena": "3.933.0",
"@planetscale/database": "1.19.0",
@@ -897,7 +897,7 @@
},
"packages/stats/server": {
"name": "@opencode-ai/stats-server",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"@aws-sdk/client-firehose": "3.933.0",
"@effect/platform-node": "catalog:",
@@ -939,7 +939,7 @@
},
"packages/tui": {
"name": "@opencode-ai/tui",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
@@ -966,7 +966,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:",
@@ -1017,7 +1017,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
diff --git a/packages/app/e2e/regression/legacy-new-session.spec.ts b/packages/app/e2e/regression/legacy-new-session.spec.ts
deleted file mode 100644
index 45cd64adcc..0000000000
--- a/packages/app/e2e/regression/legacy-new-session.spec.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { expect, test } from "@playwright/test"
-import { base64Encode } from "@opencode-ai/core/util/encode"
-import { mockOpenCodeServer } from "../utils/mock-server"
-
-const draftID = "draft_legacy_new_session"
-const directory = "C:/OpenCode/LegacyNewSession"
-const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
-
-test("redirects a draft to the legacy new-session route", async ({ page }) => {
- await mockOpenCodeServer(page, {
- directory,
- project: {
- id: "proj_legacy_new_session",
- worktree: directory,
- vcs: "git",
- name: "legacy-new-session",
- time: { created: 1700000000000, updated: 1700000000000 },
- sandboxes: [],
- },
- provider: { all: [], connected: [], default: {} },
- sessions: [],
- pageMessages: () => ({ items: [] }),
- })
- await page.addInitScript(
- ({ directory, draftID, server }) => {
- localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: false } }))
- localStorage.setItem("app-version.v1", JSON.stringify({ version: "1.17.20" }))
- localStorage.setItem(
- "opencode.window.browser.dat:tabs",
- JSON.stringify([{ type: "draft", draftID, server, directory }]),
- )
- },
- { directory, draftID, server },
- )
-
- await page.goto(`/new-session?draftId=${draftID}`)
-
- await expect(page).toHaveURL(`/${base64Encode(directory)}/session`)
- await expect(page.locator("header[data-tauri-drag-region]")).toBeVisible()
- await expect(page.locator('[data-component="prompt-input"]')).toBeVisible()
-})
diff --git a/packages/app/e2e/regression/session-timeline-projection.spec.ts b/packages/app/e2e/regression/session-timeline-projection.spec.ts
index b1aabcc32c..54c84a2bea 100644
--- a/packages/app/e2e/regression/session-timeline-projection.spec.ts
+++ b/packages/app/e2e/regression/session-timeline-projection.spec.ts
@@ -128,7 +128,7 @@ test.describe("session timeline projection", () => {
await expect(page.locator('[data-timeline-row="TurnGap"]')).toBeVisible()
})
- test("renders comment strips and historical diff summary overflow", async ({ page }) => {
+ test("renders inline comments and historical diff summary overflow", async ({ page }) => {
const user = userMessage(
[
userText("The user made the following comment regarding lines 4 through 8 of src/a.ts: Keep this stable", {
@@ -152,14 +152,10 @@ test.describe("session timeline projection", () => {
parentID: "msg_2000_diff_next_user",
created: 1700000011000,
})
- await setupTimeline(page, {
- messages: [user, assistantMessage(), nextUser, nextAssistant],
- settings: { newLayoutDesigns: false },
- })
+ await setupTimeline(page, { messages: [user, assistantMessage(), nextUser, nextAssistant] })
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
await scroller.evaluate((element) => (element.scrollTop = 0))
- await expect(page.locator('[data-timeline-row="CommentStrip"]')).toBeVisible()
await expect(page.getByText("Keep this stable", { exact: true })).toBeVisible()
await expect(page.locator('[data-timeline-row="DiffSummary"]')).toBeVisible()
await expect(page.getByText(/show all/i)).toBeVisible()
diff --git a/packages/app/package.json b/packages/app/package.json
index 17b356ed60..97c98e9fc3 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
- "version": "1.18.30",
+ "version": "1.18.31",
"description": "",
"type": "module",
"exports": {
diff --git a/packages/cli/package.json b/packages/cli/package.json
index f6dd6bf831..7e94b23108 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/cli",
- "version": "1.18.30",
+ "version": "1.18.31",
"type": "module",
"license": "MIT",
"bin": {
diff --git a/packages/codemode/package.json b/packages/codemode/package.json
index c516b3a985..8a14b5769d 100644
--- a/packages/codemode/package.json
+++ b/packages/codemode/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/codemode",
- "version": "1.18.30",
+ "version": "1.18.31",
"description": "Effect-native confined code execution over schema-described tools",
"private": true,
"type": "module",
diff --git a/packages/console/app/package.json b/packages/console/app/package.json
index 3651594fd7..b89e19a72b 100644
--- a/packages/console/app/package.json
+++ b/packages/console/app/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
- "version": "1.18.30",
+ "version": "1.18.31",
"type": "module",
"license": "MIT",
"scripts": {
diff --git a/packages/console/app/src/lib/inference-proxy.ts b/packages/console/app/src/lib/inference-proxy.ts
index b572c361b6..1eade3ff8f 100644
--- a/packages/console/app/src/lib/inference-proxy.ts
+++ b/packages/console/app/src/lib/inference-proxy.ts
@@ -1,5 +1,5 @@
import { Resource } from "@opencode-ai/console-resource"
-import { and, Database, eq, isNull, sql } from "@opencode-ai/console-core/drizzle/index.js"
+import { and, Database, eq, isNotNull, isNull, sql } from "@opencode-ai/console-core/drizzle/index.js"
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js"
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
@@ -34,7 +34,6 @@ export async function proxyInference(
: undefined)
if (!path) return undefined
- const go = url.pathname.startsWith("/zen/go/")
const key = url.pathname.endsWith("/messages")
? request.headers.get("x-api-key")
: path.startsWith("/google/")
@@ -42,44 +41,23 @@ export async function proxyInference(
: request.headers.get("authorization")?.split(" ")[1]
if (!key || key === "public") return undefined
- // Routing only; the destination owns authentication and revocation after cutover.
- const workspace = await Database.use((tx) =>
- tx
- .select({
- id: WorkspaceTable.id,
- migratedAt: WorkspaceTable.migrated_at,
- provider: ProviderTable.provider,
- })
- .from(KeyTable)
- .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID))
- .leftJoin(
- ProviderTable,
- !go && generation?.provider
- ? and(
- eq(ProviderTable.workspaceID, KeyTable.workspaceID),
- eq(ProviderTable.provider, generation.provider),
- isNull(ProviderTable.timeDeleted),
- sql`length(${ProviderTable.credentials}) > 0`,
- )
- : sql`false`,
- )
- .where(eq(KeyTable.key, key))
- .limit(1)
- .then((rows) => rows[0]),
- )
- if (!workspace?.migratedAt) return undefined
- const model = workspace.provider ? generation?.model : undefined
- if (workspace.provider && !model) throw new Error("Legacy BYOK model mapping is unavailable")
+ // New Console keys are never in the legacy key table; every legacy key is `sk-`.
+ const legacy = !key.startsWith("oc_sk_")
+ const workspace = legacy ? await migratedWorkspace(key, generation?.provider) : undefined
+ if (legacy && !workspace) return undefined
+ const model = workspace?.provider ? generation?.model : undefined
+ if (workspace?.provider && !model) throw new Error("Legacy BYOK model mapping is unavailable")
const destination = new URL(Resource.ConsoleMigration.inferenceUrl)
// Imported connections must use this same workspace/provider-derived ID.
- const target = model
- ? `/custom/conn_${workspace.id.slice(4)}_${workspace.provider}${
- path.startsWith("/google/")
- ? `/models/${encodeURIComponent(model)}${url.pathname.slice(url.pathname.lastIndexOf(":"))}`
- : url.pathname.slice("/zen/v1".length)
- }`
- : path
+ const target =
+ model && workspace
+ ? `/custom/conn_${workspace.id.slice(4)}_${workspace.provider}${
+ path.startsWith("/google/")
+ ? `/models/${encodeURIComponent(model)}${url.pathname.slice(url.pathname.lastIndexOf(":"))}`
+ : url.pathname.slice("/zen/v1".length)
+ }`
+ : path
destination.pathname = `${destination.pathname.replace(/\/$/, "")}${target}`
destination.search = url.search
destination.hash = ""
@@ -109,6 +87,30 @@ export async function proxyInference(
return fetch(forwarded, { redirect: "manual" })
}
+// Routing only; the destination owns authentication and revocation after cutover.
+function migratedWorkspace(key: string, provider?: string) {
+ return Database.use((tx) =>
+ tx
+ .select({ id: WorkspaceTable.id, provider: ProviderTable.provider })
+ .from(KeyTable)
+ .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID))
+ .leftJoin(
+ ProviderTable,
+ provider
+ ? and(
+ eq(ProviderTable.workspaceID, KeyTable.workspaceID),
+ eq(ProviderTable.provider, provider),
+ isNull(ProviderTable.timeDeleted),
+ sql`length(${ProviderTable.credentials}) > 0`,
+ )
+ : sql`false`,
+ )
+ .where(and(eq(KeyTable.key, key), isNotNull(WorkspaceTable.migrated_at)))
+ .limit(1)
+ .then((rows) => rows[0]),
+ )
+}
+
export function inferenceUnavailable() {
return Response.json(
{ error: { type: "api_error", message: "Inference routing is unavailable. Please retry later." } },
diff --git a/packages/console/core/package.json b/packages/console/core/package.json
index 19aade616d..fefbe8abfd 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.18.30",
+ "version": "1.18.31",
"private": true,
"type": "module",
"license": "MIT",
diff --git a/packages/console/function/package.json b/packages/console/function/package.json
index d77be8cb6a..1026754e63 100644
--- a/packages/console/function/package.json
+++ b/packages/console/function/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
- "version": "1.18.30",
+ "version": "1.18.31",
"$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 e35574b3e9..755102b6e7 100644
--- a/packages/console/mail/package.json
+++ b/packages/console/mail/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
- "version": "1.18.30",
+ "version": "1.18.31",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
diff --git a/packages/console/support/package.json b/packages/console/support/package.json
index 75e8e07147..56a0099ad4 100644
--- a/packages/console/support/package.json
+++ b/packages/console/support/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-support",
- "version": "1.18.30",
+ "version": "1.18.31",
"type": "module",
"license": "MIT",
"scripts": {
diff --git a/packages/core/package.json b/packages/core/package.json
index ab20288b52..2b465e5852 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
- "version": "1.18.30",
+ "version": "1.18.31",
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",
diff --git a/packages/desktop/package.json b/packages/desktop/package.json
index 7c55897b74..f494e53416 100644
--- a/packages/desktop/package.json
+++ b/packages/desktop/package.json
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
- "version": "1.18.30",
+ "version": "1.18.31",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
diff --git a/packages/effect-drizzle-sqlite/package.json b/packages/effect-drizzle-sqlite/package.json
index 35cc50f97d..1dc5d0ce66 100644
--- a/packages/effect-drizzle-sqlite/package.json
+++ b/packages/effect-drizzle-sqlite/package.json
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
- "version": "1.18.30",
+ "version": "1.18.31",
"name": "@opencode-ai/effect-drizzle-sqlite",
"type": "module",
"license": "MIT",
diff --git a/packages/effect-sqlite-node/package.json b/packages/effect-sqlite-node/package.json
index 19d0652c1a..5a425b9feb 100644
--- a/packages/effect-sqlite-node/package.json
+++ b/packages/effect-sqlite-node/package.json
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
- "version": "1.18.30",
+ "version": "1.18.31",
"name": "@opencode-ai/effect-sqlite-node",
"type": "module",
"license": "MIT",
diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json
index 9d61ae5eac..aea997e34e 100644
--- a/packages/enterprise/package.json
+++ b/packages/enterprise/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
- "version": "1.18.30",
+ "version": "1.18.31",
"private": true,
"type": "module",
"license": "MIT",
diff --git a/packages/function/package.json b/packages/function/package.json
index 7d9bc6548b..03348052a2 100644
--- a/packages/function/package.json
+++ b/packages/function/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
- "version": "1.18.30",
+ "version": "1.18.31",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json
index cc8ae63655..1fd0b834eb 100644
--- a/packages/http-recorder/package.json
+++ b/packages/http-recorder/package.json
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
- "version": "1.18.30",
+ "version": "1.18.31",
"name": "@opencode-ai/http-recorder",
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
"type": "module",
diff --git a/packages/llm/package.json b/packages/llm/package.json
index 79471820d6..2b10849540 100644
--- a/packages/llm/package.json
+++ b/packages/llm/package.json
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
- "version": "1.18.30",
+ "version": "1.18.31",
"name": "@opencode-ai/llm",
"type": "module",
"license": "MIT",
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index 8cc027275c..5e628b3c70 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
- "version": "1.18.30",
+ "version": "1.18.31",
"name": "opencode",
"type": "module",
"license": "MIT",
diff --git a/packages/opencode/src/cli/cmd/tui.ts b/packages/opencode/src/cli/cmd/tui.ts
index 95ffac7ea5..d5fa46bfb1 100644
--- a/packages/opencode/src/cli/cmd/tui.ts
+++ b/packages/opencode/src/cli/cmd/tui.ts
@@ -303,7 +303,7 @@ export const TuiThreadCommand = cmd({
unguard?.()
} catch {}
}
- process.exit(0)
+ process.exit()
},
})
// scratch
diff --git a/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts b/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts
index 21b53aba75..3bedd19704 100644
--- a/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts
+++ b/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts
@@ -20,7 +20,8 @@ export const errorLayer = HttpRouter.middleware<{ handles: unknown }>()((effect)
ConfigErrorV1.JsonError.isInstance(error) ||
ConfigErrorV1.InvalidError.isInstance(error) ||
ConfigErrorV1.FrontmatterError.isInstance(error) ||
- ConfigErrorV1.DirectoryTypoError.isInstance(error)
+ ConfigErrorV1.DirectoryTypoError.isInstance(error) ||
+ ConfigErrorV1.RemoteAuthError.isInstance(error)
) {
return Effect.succeed(HttpServerResponse.jsonUnsafe(error.toObject(), { status: 400 }))
}
diff --git a/packages/opencode/test/server/httpapi-error-middleware.test.ts b/packages/opencode/test/server/httpapi-error-middleware.test.ts
index 7bfc6da032..525bf34e20 100644
--- a/packages/opencode/test/server/httpapi-error-middleware.test.ts
+++ b/packages/opencode/test/server/httpapi-error-middleware.test.ts
@@ -83,6 +83,27 @@ describe("HttpApi error middleware", () => {
}),
)
+ it.live("returns remote auth defects as structured client errors", () =>
+ Effect.gen(function* () {
+ const configError = new ConfigErrorV1.RemoteAuthError({
+ url: "https://example.com",
+ remote: "https://config.example.com/opencode.json",
+ })
+
+ yield* HttpRouter.add("GET", "/remote-auth-error", Effect.die(configError)).pipe(
+ Layer.provide(errorLayer),
+ HttpRouter.serve,
+ Layer.build,
+ )
+
+ const response = yield* HttpClientRequest.get("/remote-auth-error").pipe(HttpClient.execute)
+ const body = yield* response.json
+
+ expect(response.status).toBe(400)
+ expect(body).toEqual(configError.toObject())
+ }),
+ )
+
it.live("does not map storage not-found defects to 404", () =>
Effect.gen(function* () {
yield* HttpRouter.add(
diff --git a/packages/plugin/package.json b/packages/plugin/package.json
index d06914cdc8..09c1210988 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.18.30",
+ "version": "1.18.31",
"type": "module",
"license": "MIT",
"scripts": {
diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json
index 9777184dfe..a471612eca 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.18.30",
+ "version": "1.18.31",
"type": "module",
"license": "MIT",
"scripts": {
diff --git a/packages/server/package.json b/packages/server/package.json
index 04cb235ff6..c99bd4ac87 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/server",
- "version": "1.18.30",
+ "version": "1.18.31",
"private": true,
"type": "module",
"license": "MIT",
diff --git a/packages/session-ui/package.json b/packages/session-ui/package.json
index 7db27630da..aaba6a52d8 100644
--- a/packages/session-ui/package.json
+++ b/packages/session-ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/session-ui",
- "version": "1.18.30",
+ "version": "1.18.31",
"private": true,
"type": "module",
"license": "MIT",
diff --git a/packages/slack/package.json b/packages/slack/package.json
index f1aa4602ba..86fc0d763a 100644
--- a/packages/slack/package.json
+++ b/packages/slack/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
- "version": "1.18.30",
+ "version": "1.18.31",
"type": "module",
"license": "MIT",
"scripts": {
diff --git a/packages/stats/app/package.json b/packages/stats/app/package.json
index 20d56aa596..bd1064c18c 100644
--- a/packages/stats/app/package.json
+++ b/packages/stats/app/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/stats-app",
- "version": "1.18.30",
+ "version": "1.18.31",
"private": true,
"type": "module",
"license": "MIT",
diff --git a/packages/stats/core/package.json b/packages/stats/core/package.json
index d6ffa5fd65..3bfb642ea2 100644
--- a/packages/stats/core/package.json
+++ b/packages/stats/core/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/stats-core",
- "version": "1.18.30",
+ "version": "1.18.31",
"private": true,
"type": "module",
"license": "MIT",
diff --git a/packages/stats/server/package.json b/packages/stats/server/package.json
index 27f5e23ec0..26efcc3aae 100644
--- a/packages/stats/server/package.json
+++ b/packages/stats/server/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/stats-server",
- "version": "1.18.30",
+ "version": "1.18.31",
"private": true,
"type": "module",
"license": "MIT",
diff --git a/packages/tui/package.json b/packages/tui/package.json
index 4b9b95b35e..bc8c1181e9 100644
--- a/packages/tui/package.json
+++ b/packages/tui/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/tui",
- "version": "1.18.30",
+ "version": "1.18.31",
"private": true,
"type": "module",
"license": "MIT",
diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx
index 3f1da522bb..6852311f9e 100644
--- a/packages/tui/src/app.tsx
+++ b/packages/tui/src/app.tsx
@@ -356,8 +356,10 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
)
yield* Effect.sync(() => {
win32FlushInputBuffer()
- if (result.reason !== undefined)
+ if (result.reason !== undefined) {
process.stderr.write((cliErrorMessage(result.reason) ?? errorFormat(result.reason)) + "\n")
+ process.exitCode = 1
+ }
if (result.epilogue) process.stdout.write(result.epilogue + "\n")
})
})
diff --git a/packages/tui/src/util/error.ts b/packages/tui/src/util/error.ts
index 757758a205..606bfb3bb7 100644
--- a/packages/tui/src/util/error.ts
+++ b/packages/tui/src/util/error.ts
@@ -47,6 +47,17 @@ export function cliErrorMessage(input: unknown): string | undefined {
const frontmatter = configData(input, "ConfigFrontmatterError")
if (frontmatter) return field(frontmatter, "message") ?? ""
+ const remoteAuth = configData(input, "ConfigRemoteAuthError")
+ if (remoteAuth) {
+ const url = field(remoteAuth, "url")
+ const remote = field(remoteAuth, "remote")
+ return [
+ `Failed to load remote config${remote ? ` from ${remote}` : ""}: the server returned a login page instead of JSON.`,
+ "Authentication is missing or has expired (the endpoint is likely behind an SSO or identity-aware proxy).",
+ ...(url ? [`Run \`opencode auth login ${url}\` to re-authenticate.`] : []),
+ ].join("\n")
+ }
+
const invalid = configData(input, "ConfigInvalidError")
if (invalid) {
const path = field(invalid, "path")
diff --git a/packages/tui/test/app-lifecycle.test.tsx b/packages/tui/test/app-lifecycle.test.tsx
index 5706634247..8cd40315cc 100644
--- a/packages/tui/test/app-lifecycle.test.tsx
+++ b/packages/tui/test/app-lifecycle.test.tsx
@@ -126,3 +126,65 @@ test("app.exit prints the session epilogue after scoped cleanup", async () => {
mock.restore()
}
})
+
+test("fatal startup errors set a nonzero exit after scoped cleanup", async () => {
+ const setup = await createTestRenderer({ width: 80, height: 24, useThread: false })
+ const core = await import("@opentui/core")
+ await mock.module("@opentui/core", () => ({ ...core, createCliRenderer: async () => setup.renderer }))
+ const events = createEventSource()
+ const calls = createFetch((url) => {
+ if (url.pathname === "/config")
+ return json(
+ {
+ name: "ConfigRemoteAuthError",
+ data: {
+ url: "https://example.com",
+ remote: "https://config.example.com/opencode.json",
+ },
+ },
+ { status: 400 },
+ )
+ return undefined
+ })
+ let disposes = 0
+ const originalWrite = process.stderr.write.bind(process.stderr)
+ const originalExitCode = process.exitCode ?? 0
+ let stderr = ""
+
+ process.stderr.write = ((chunk: string | Uint8Array) => {
+ stderr += String(chunk)
+ return true
+ }) as typeof process.stderr.write
+
+ try {
+ const { run } = await import("../src/app")
+ const task = Effect.runPromise(
+ run({
+ url: "http://test",
+ directory,
+ config: createTuiResolvedConfig({ plugin_enabled: {} }),
+ fetch: calls.fetch,
+ events: events.source,
+ args: {},
+ pluginHost: {
+ async start() {},
+ async dispose() {
+ disposes++
+ },
+ },
+ }).pipe(Effect.provide(AppNodeBuilder.build(Global.node))),
+ )
+
+ await task
+ expect(stderr).toContain("Run `opencode auth login https://example.com` to re-authenticate.")
+ expect(stderr).not.toContain("Unexpected server error")
+ expect(process.exitCode).toBe(1)
+ expect(setup.renderer.isDestroyed).toBe(true)
+ expect(disposes).toBe(1)
+ } finally {
+ process.stderr.write = originalWrite
+ process.exitCode = originalExitCode
+ if (!setup.renderer.isDestroyed) setup.renderer.destroy()
+ mock.restore()
+ }
+})
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 28dbce9eca..02c1573f0d 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
- "version": "1.18.30",
+ "version": "1.18.31",
"type": "module",
"license": "MIT",
"repository": {
diff --git a/packages/ui/src/assets/icons/provider/merge-gateway.svg b/packages/ui/src/assets/icons/provider/merge-gateway.svg
new file mode 100644
index 0000000000..a219b55bf9
--- /dev/null
+++ b/packages/ui/src/assets/icons/provider/merge-gateway.svg
@@ -0,0 +1,4 @@
+
diff --git a/packages/ui/src/components/provider-icons/sprite.svg b/packages/ui/src/components/provider-icons/sprite.svg
index c8099d7ad1..3922263234 100644
--- a/packages/ui/src/components/provider-icons/sprite.svg
+++ b/packages/ui/src/components/provider-icons/sprite.svg
@@ -576,6 +576,14 @@
stroke-linejoin="round"
>
+
+
+
+