Skip to content

Commit 9d5f3c1

Browse files
authored
fix(workspaces): surface real error messages on failed workspace operations (#29167)
Signed-off-by: James Murdza <james@jamesmurdza.com>
1 parent 7342e94 commit 9d5f3c1

5 files changed

Lines changed: 84 additions & 27 deletions

File tree

packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,22 @@ export function DialogSessionList() {
5353
const workspaceID = await (async () => {
5454
if (selection.type === "none") return null
5555
if (selection.type === "existing") return selection.workspaceID
56-
const result = await sdk.client.experimental.workspace
57-
.create({ type: selection.workspaceType, branch: null })
58-
.catch(() => undefined)
56+
let result
57+
try {
58+
result = await sdk.client.experimental.workspace.create({ type: selection.workspaceType, branch: null })
59+
} catch (err) {
60+
toast.show({
61+
title: "Failed to create workspace",
62+
message: errorMessage(err),
63+
variant: "error",
64+
})
65+
return
66+
}
5967
const workspace = result?.data
6068
if (!workspace) {
6169
toast.show({
62-
message: `Failed to create workspace: ${errorMessage(result?.error ?? "no response")}`,
70+
title: "Failed to create workspace",
71+
message: errorMessage(result?.error ?? "no response"),
6372
variant: "error",
6473
})
6574
return

packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,17 @@ async function loadWorkspaceAdapters(input: {
6161
const dir = input.sync.path.directory || input.sdk.directory
6262
const url = new URL("/experimental/workspace/adapter", input.sdk.url)
6363
if (dir) url.searchParams.set("directory", dir)
64-
const res = await input.sdk
65-
.fetch(url)
66-
.then((x) => x.json() as Promise<Adapter[]>)
67-
.catch(() => undefined)
68-
if (res) return res
69-
input.toast.show({
70-
message: "Failed to load workspace adapters",
71-
variant: "error",
72-
})
64+
try {
65+
const response = await input.sdk.fetch(url)
66+
return (await response.json()) as Adapter[]
67+
} catch (err) {
68+
input.toast.show({
69+
title: "Failed to load workspace adapters",
70+
message: errorMessage(err),
71+
variant: "error",
72+
})
73+
return undefined
74+
}
7375
}
7476

7577
export async function openWorkspaceSelect(input: {
@@ -100,13 +102,21 @@ export async function warpWorkspaceSession(input: {
100102
copyChanges: boolean
101103
done?: () => void
102104
}): Promise<boolean> {
103-
const result = await input.sdk.client.experimental.workspace
104-
.warp({
105+
let result
106+
try {
107+
result = await input.sdk.client.experimental.workspace.warp({
105108
id: input.workspaceID,
106109
sessionID: input.sessionID,
107110
copyChanges: input.copyChanges,
108111
})
109-
.catch(() => undefined)
112+
} catch (err) {
113+
input.toast.show({
114+
title: "Failed to warp session",
115+
message: errorMessage(err),
116+
variant: "error",
117+
})
118+
return false
119+
}
110120
if (!result?.data) {
111121
if (result?.error && "name" in result.error && result.error.name === "VcsApplyError") {
112122
await DialogAlert.show(
@@ -118,7 +128,8 @@ export async function warpWorkspaceSession(input: {
118128
}
119129

120130
input.toast.show({
121-
message: `Failed to warp session: ${errorMessage(result?.error ?? "no response")}`,
131+
title: "Failed to warp session",
132+
message: errorMessage(result?.error ?? "no response"),
122133
variant: "error",
123134
})
124135
return false

packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import type { AssistantMessage, FilePart, UserMessage } from "@opencode-ai/sdk/v
4040
import { TuiEvent } from "../../event"
4141
import { iife } from "@/util/iife"
4242
import { Locale } from "@/util/locale"
43+
import { errorMessage } from "@/util/error"
4344
import { formatDuration } from "@/util/format"
4445
import { createColors, createFrames } from "../../ui/spinner.ts"
4546
import { useDialog } from "@tui/ui/dialog"
@@ -216,14 +217,25 @@ export function Prompt(props: PromptProps) {
216217

217218
async function createWorkspace(selection: Extract<WorkspaceSelection, { type: "new" }>) {
218219
setCreatingWorkspace(true)
219-
const result = await sdk.client.experimental.workspace
220-
.create({ type: selection.workspaceType, branch: null })
221-
.catch(() => undefined)
222-
if (result == undefined || result.error || !result.data) {
220+
let result
221+
try {
222+
result = await sdk.client.experimental.workspace.create({ type: selection.workspaceType, branch: null })
223+
} catch (err) {
224+
selectWorkspace(undefined)
225+
setCreatingWorkspace(false)
226+
toast.show({
227+
title: "Creating workspace failed",
228+
message: errorMessage(err),
229+
variant: "error",
230+
})
231+
return
232+
}
233+
if (result.error || !result.data) {
223234
selectWorkspace(undefined)
224235
setCreatingWorkspace(false)
225236
toast.show({
226-
message: "Creating workspace failed",
237+
title: "Creating workspace failed",
238+
message: errorMessage(result.error ?? "no response"),
227239
variant: "error",
228240
})
229241
return

packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ export class ApiWorkspaceWarpError extends Schema.ErrorClass<ApiWorkspaceWarpErr
2727
{ httpApiStatus: 400 },
2828
) {}
2929

30+
export class ApiWorkspaceCreateError extends Schema.ErrorClass<ApiWorkspaceCreateError>("WorkspaceCreateError")(
31+
{
32+
name: Schema.Literal("WorkspaceCreateError"),
33+
data: Schema.Struct({
34+
message: Schema.String,
35+
}),
36+
},
37+
{ httpApiStatus: 400 },
38+
) {}
39+
3040
export const WorkspacePaths = {
3141
adapters: `${root}/adapter`,
3242
list: root,
@@ -64,7 +74,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
6474
query: WorkspaceRoutingQuery,
6575
payload: CreatePayload,
6676
success: described(Workspace.Info, "Workspace created"),
67-
error: HttpApiError.BadRequest,
77+
error: [ApiWorkspaceCreateError, HttpApiError.BadRequest],
6878
}).annotateMerge(
6979
OpenApi.annotations({
7080
identifier: "experimental.workspace.create",

packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import { listAdapters } from "@/control-plane/adapters"
22
import { Workspace } from "@/control-plane/workspace"
33
import * as InstanceState from "@/effect/instance-state"
44
import { Vcs } from "@/project/vcs"
5-
import { Effect } from "effect"
6-
import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi"
5+
import { Cause, Effect } from "effect"
6+
import { HttpApiBuilder } from "effect/unstable/httpapi"
77
import { InstanceHttpApi } from "../api"
88
import { notFound } from "../errors"
99
import { ApiVcsApplyError } from "../groups/instance"
10-
import { ApiWorkspaceWarpError, CreatePayload, WarpPayload } from "../groups/workspace"
10+
import { ApiWorkspaceCreateError, ApiWorkspaceWarpError, CreatePayload, WarpPayload } from "../groups/workspace"
1111

1212
export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspace", (handlers) =>
1313
Effect.gen(function* () {
@@ -30,7 +30,22 @@ export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspac
3030
extra: ctx.payload.extra ?? null,
3131
projectID: instance.project.id,
3232
})
33-
.pipe(Effect.mapError(() => new HttpApiError.BadRequest({})))
33+
.pipe(
34+
Effect.catchCause((cause) => {
35+
// Plugin throws surface as defects (because EffectBridge.fromPromise uses Effect.promise),
36+
// bypassing Effect.mapError. Walk the cause to surface the real error to the client.
37+
const die = cause.reasons.find(Cause.isDieReason)
38+
const fail = cause.reasons.find(Cause.isFailReason)
39+
const reason: unknown = die?.defect ?? fail?.error
40+
const message = reason instanceof Error ? reason.message : "Workspace creation failed"
41+
return Effect.fail(
42+
new ApiWorkspaceCreateError({
43+
name: "WorkspaceCreateError",
44+
data: { message },
45+
}),
46+
)
47+
}),
48+
)
3449
})
3550

3651
const syncList = Effect.fn("WorkspaceHttpApi.syncList")(function* () {

0 commit comments

Comments
 (0)