From 1dda9018fe13abfae96648e7188ea2f5d5773d68 Mon Sep 17 00:00:00 2001 From: James Murdza Date: Sun, 24 May 2026 21:24:51 -0700 Subject: [PATCH 1/2] fix(workspaces): surface plugin errors from workspace create endpoint Plugin throws reach the create handler as Effect defects (EffectBridge uses Effect.promise), so they bypassed Effect.mapError and surfaced as a generic error. Catch the cause, extract the original message, and return a typed ApiWorkspaceCreateError so the real reason reaches the client. Signed-off-by: James Murdza --- .../instance/httpapi/groups/workspace.ts | 12 +++++++++- .../instance/httpapi/handlers/workspace.ts | 23 +++++++++++++++---- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts index 6a5101dc4219..09a6e67b6e9b 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts @@ -27,6 +27,16 @@ export class ApiWorkspaceWarpError extends Schema.ErrorClass("WorkspaceCreateError")( + { + name: Schema.Literal("WorkspaceCreateError"), + data: Schema.Struct({ + message: Schema.String, + }), + }, + { httpApiStatus: 400 }, +) {} + export const WorkspacePaths = { adapters: `${root}/adapter`, list: root, @@ -64,7 +74,7 @@ export const WorkspaceApi = HttpApi.make("workspace") query: WorkspaceRoutingQuery, payload: CreatePayload, success: described(Workspace.Info, "Workspace created"), - error: HttpApiError.BadRequest, + error: [ApiWorkspaceCreateError, HttpApiError.BadRequest], }).annotateMerge( OpenApi.annotations({ identifier: "experimental.workspace.create", diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts index 2699c8659040..7f5c437d9d58 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts @@ -2,12 +2,12 @@ import { listAdapters } from "@/control-plane/adapters" import { Workspace } from "@/control-plane/workspace" import * as InstanceState from "@/effect/instance-state" import { Vcs } from "@/project/vcs" -import { Effect } from "effect" -import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi" +import { Cause, Effect } from "effect" +import { HttpApiBuilder } from "effect/unstable/httpapi" import { InstanceHttpApi } from "../api" import { notFound } from "../errors" import { ApiVcsApplyError } from "../groups/instance" -import { ApiWorkspaceWarpError, CreatePayload, WarpPayload } from "../groups/workspace" +import { ApiWorkspaceCreateError, ApiWorkspaceWarpError, CreatePayload, WarpPayload } from "../groups/workspace" export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspace", (handlers) => Effect.gen(function* () { @@ -30,7 +30,22 @@ export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspac extra: ctx.payload.extra ?? null, projectID: instance.project.id, }) - .pipe(Effect.mapError(() => new HttpApiError.BadRequest({}))) + .pipe( + Effect.catchCause((cause) => { + // Plugin throws surface as defects (because EffectBridge.fromPromise uses Effect.promise), + // bypassing Effect.mapError. Walk the cause to surface the real error to the client. + const die = cause.reasons.find(Cause.isDieReason) + const fail = cause.reasons.find(Cause.isFailReason) + const reason: unknown = die?.defect ?? fail?.error + const message = reason instanceof Error ? reason.message : "Workspace creation failed" + return Effect.fail( + new ApiWorkspaceCreateError({ + name: "WorkspaceCreateError", + data: { message }, + }), + ) + }), + ) }) const syncList = Effect.fn("WorkspaceHttpApi.syncList")(function* () { From 03af917292400ed5c299a02d6ab86ba14f1e360c Mon Sep 17 00:00:00 2001 From: James Murdza Date: Sun, 24 May 2026 21:24:59 -0700 Subject: [PATCH 2/2] fix(workspaces): display actual error messages instead of generic failures Replace .catch(() => undefined) and detail-less toasts in the workspace create, warp, and adapter-load flows with try/catch that surfaces the real error via errorMessage(), shown as a title + message toast. Signed-off-by: James Murdza --- .../cmd/tui/component/dialog-session-list.tsx | 17 +++++++-- .../tui/component/dialog-workspace-create.tsx | 37 ++++++++++++------- .../cli/cmd/tui/component/prompt/index.tsx | 22 ++++++++--- 3 files changed, 54 insertions(+), 22 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx index 17653af6b9a9..e16893353a80 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx @@ -53,13 +53,22 @@ export function DialogSessionList() { const workspaceID = await (async () => { if (selection.type === "none") return null if (selection.type === "existing") return selection.workspaceID - const result = await sdk.client.experimental.workspace - .create({ type: selection.workspaceType, branch: null }) - .catch(() => undefined) + let result + try { + result = await sdk.client.experimental.workspace.create({ type: selection.workspaceType, branch: null }) + } catch (err) { + toast.show({ + title: "Failed to create workspace", + message: errorMessage(err), + variant: "error", + }) + return + } const workspace = result?.data if (!workspace) { toast.show({ - message: `Failed to create workspace: ${errorMessage(result?.error ?? "no response")}`, + title: "Failed to create workspace", + message: errorMessage(result?.error ?? "no response"), variant: "error", }) return diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx index b22930bc6c1a..29566c46102a 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx @@ -61,15 +61,17 @@ async function loadWorkspaceAdapters(input: { const dir = input.sync.path.directory || input.sdk.directory const url = new URL("/experimental/workspace/adapter", input.sdk.url) if (dir) url.searchParams.set("directory", dir) - const res = await input.sdk - .fetch(url) - .then((x) => x.json() as Promise) - .catch(() => undefined) - if (res) return res - input.toast.show({ - message: "Failed to load workspace adapters", - variant: "error", - }) + try { + const response = await input.sdk.fetch(url) + return (await response.json()) as Adapter[] + } catch (err) { + input.toast.show({ + title: "Failed to load workspace adapters", + message: errorMessage(err), + variant: "error", + }) + return undefined + } } export async function openWorkspaceSelect(input: { @@ -100,13 +102,21 @@ export async function warpWorkspaceSession(input: { copyChanges: boolean done?: () => void }): Promise { - const result = await input.sdk.client.experimental.workspace - .warp({ + let result + try { + result = await input.sdk.client.experimental.workspace.warp({ id: input.workspaceID, sessionID: input.sessionID, copyChanges: input.copyChanges, }) - .catch(() => undefined) + } catch (err) { + input.toast.show({ + title: "Failed to warp session", + message: errorMessage(err), + variant: "error", + }) + return false + } if (!result?.data) { if (result?.error && "name" in result.error && result.error.name === "VcsApplyError") { await DialogAlert.show( @@ -118,7 +128,8 @@ export async function warpWorkspaceSession(input: { } input.toast.show({ - message: `Failed to warp session: ${errorMessage(result?.error ?? "no response")}`, + title: "Failed to warp session", + message: errorMessage(result?.error ?? "no response"), variant: "error", }) return false diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 0566e07b3451..ce7b300b7b10 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -40,6 +40,7 @@ import type { AssistantMessage, FilePart, UserMessage } from "@opencode-ai/sdk/v import { TuiEvent } from "../../event" import { iife } from "@/util/iife" import { Locale } from "@/util/locale" +import { errorMessage } from "@/util/error" import { formatDuration } from "@/util/format" import { createColors, createFrames } from "../../ui/spinner.ts" import { useDialog } from "@tui/ui/dialog" @@ -216,14 +217,25 @@ export function Prompt(props: PromptProps) { async function createWorkspace(selection: Extract) { setCreatingWorkspace(true) - const result = await sdk.client.experimental.workspace - .create({ type: selection.workspaceType, branch: null }) - .catch(() => undefined) - if (result == undefined || result.error || !result.data) { + let result + try { + result = await sdk.client.experimental.workspace.create({ type: selection.workspaceType, branch: null }) + } catch (err) { + selectWorkspace(undefined) + setCreatingWorkspace(false) + toast.show({ + title: "Creating workspace failed", + message: errorMessage(err), + variant: "error", + }) + return + } + if (result.error || !result.data) { selectWorkspace(undefined) setCreatingWorkspace(false) toast.show({ - message: "Creating workspace failed", + title: "Creating workspace failed", + message: errorMessage(result.error ?? "no response"), variant: "error", }) return