fix(workspaces): surface real error messages on failed workspace operations (#29167)

Signed-off-by: James Murdza <james@jamesmurdza.com>
This commit is contained in:
James Murdza
2026-05-29 14:00:23 -07:00
committed by GitHub
parent 7342e94094
commit 9d5f3c1833
5 changed files with 84 additions and 27 deletions
@@ -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
@@ -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<Adapter[]>)
.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<boolean> {
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
@@ -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<WorkspaceSelection, { type: "new" }>) {
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({
message: "Creating workspace failed",
title: "Creating workspace failed",
message: errorMessage(err),
variant: "error",
})
return
}
if (result.error || !result.data) {
selectWorkspace(undefined)
setCreatingWorkspace(false)
toast.show({
title: "Creating workspace failed",
message: errorMessage(result.error ?? "no response"),
variant: "error",
})
return
@@ -27,6 +27,16 @@ export class ApiWorkspaceWarpError extends Schema.ErrorClass<ApiWorkspaceWarpErr
{ httpApiStatus: 400 },
) {}
export class ApiWorkspaceCreateError extends Schema.ErrorClass<ApiWorkspaceCreateError>("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",
@@ -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* () {