mirror of
https://github.com/anomalyco/opencode.git
synced 2026-06-02 06:16:48 +02:00
fix(workspaces): surface real error messages on failed workspace operations (#29167)
Signed-off-by: James Murdza <james@jamesmurdza.com>
This commit is contained in:
@@ -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* () {
|
||||
|
||||
Reference in New Issue
Block a user