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 () => {
|
const workspaceID = await (async () => {
|
||||||
if (selection.type === "none") return null
|
if (selection.type === "none") return null
|
||||||
if (selection.type === "existing") return selection.workspaceID
|
if (selection.type === "existing") return selection.workspaceID
|
||||||
const result = await sdk.client.experimental.workspace
|
let result
|
||||||
.create({ type: selection.workspaceType, branch: null })
|
try {
|
||||||
.catch(() => undefined)
|
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
|
const workspace = result?.data
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
toast.show({
|
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",
|
variant: "error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -61,15 +61,17 @@ async function loadWorkspaceAdapters(input: {
|
|||||||
const dir = input.sync.path.directory || input.sdk.directory
|
const dir = input.sync.path.directory || input.sdk.directory
|
||||||
const url = new URL("/experimental/workspace/adapter", input.sdk.url)
|
const url = new URL("/experimental/workspace/adapter", input.sdk.url)
|
||||||
if (dir) url.searchParams.set("directory", dir)
|
if (dir) url.searchParams.set("directory", dir)
|
||||||
const res = await input.sdk
|
try {
|
||||||
.fetch(url)
|
const response = await input.sdk.fetch(url)
|
||||||
.then((x) => x.json() as Promise<Adapter[]>)
|
return (await response.json()) as Adapter[]
|
||||||
.catch(() => undefined)
|
} catch (err) {
|
||||||
if (res) return res
|
input.toast.show({
|
||||||
input.toast.show({
|
title: "Failed to load workspace adapters",
|
||||||
message: "Failed to load workspace adapters",
|
message: errorMessage(err),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
})
|
})
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openWorkspaceSelect(input: {
|
export async function openWorkspaceSelect(input: {
|
||||||
@@ -100,13 +102,21 @@ export async function warpWorkspaceSession(input: {
|
|||||||
copyChanges: boolean
|
copyChanges: boolean
|
||||||
done?: () => void
|
done?: () => void
|
||||||
}): Promise<boolean> {
|
}): Promise<boolean> {
|
||||||
const result = await input.sdk.client.experimental.workspace
|
let result
|
||||||
.warp({
|
try {
|
||||||
|
result = await input.sdk.client.experimental.workspace.warp({
|
||||||
id: input.workspaceID,
|
id: input.workspaceID,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
copyChanges: input.copyChanges,
|
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?.data) {
|
||||||
if (result?.error && "name" in result.error && result.error.name === "VcsApplyError") {
|
if (result?.error && "name" in result.error && result.error.name === "VcsApplyError") {
|
||||||
await DialogAlert.show(
|
await DialogAlert.show(
|
||||||
@@ -118,7 +128,8 @@ export async function warpWorkspaceSession(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input.toast.show({
|
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",
|
variant: "error",
|
||||||
})
|
})
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import type { AssistantMessage, FilePart, UserMessage } from "@opencode-ai/sdk/v
|
|||||||
import { TuiEvent } from "../../event"
|
import { TuiEvent } from "../../event"
|
||||||
import { iife } from "@/util/iife"
|
import { iife } from "@/util/iife"
|
||||||
import { Locale } from "@/util/locale"
|
import { Locale } from "@/util/locale"
|
||||||
|
import { errorMessage } from "@/util/error"
|
||||||
import { formatDuration } from "@/util/format"
|
import { formatDuration } from "@/util/format"
|
||||||
import { createColors, createFrames } from "../../ui/spinner.ts"
|
import { createColors, createFrames } from "../../ui/spinner.ts"
|
||||||
import { useDialog } from "@tui/ui/dialog"
|
import { useDialog } from "@tui/ui/dialog"
|
||||||
@@ -216,14 +217,25 @@ export function Prompt(props: PromptProps) {
|
|||||||
|
|
||||||
async function createWorkspace(selection: Extract<WorkspaceSelection, { type: "new" }>) {
|
async function createWorkspace(selection: Extract<WorkspaceSelection, { type: "new" }>) {
|
||||||
setCreatingWorkspace(true)
|
setCreatingWorkspace(true)
|
||||||
const result = await sdk.client.experimental.workspace
|
let result
|
||||||
.create({ type: selection.workspaceType, branch: null })
|
try {
|
||||||
.catch(() => undefined)
|
result = await sdk.client.experimental.workspace.create({ type: selection.workspaceType, branch: null })
|
||||||
if (result == undefined || result.error || !result.data) {
|
} catch (err) {
|
||||||
selectWorkspace(undefined)
|
selectWorkspace(undefined)
|
||||||
setCreatingWorkspace(false)
|
setCreatingWorkspace(false)
|
||||||
toast.show({
|
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",
|
variant: "error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -27,6 +27,16 @@ export class ApiWorkspaceWarpError extends Schema.ErrorClass<ApiWorkspaceWarpErr
|
|||||||
{ httpApiStatus: 400 },
|
{ 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 = {
|
export const WorkspacePaths = {
|
||||||
adapters: `${root}/adapter`,
|
adapters: `${root}/adapter`,
|
||||||
list: root,
|
list: root,
|
||||||
@@ -64,7 +74,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
|
|||||||
query: WorkspaceRoutingQuery,
|
query: WorkspaceRoutingQuery,
|
||||||
payload: CreatePayload,
|
payload: CreatePayload,
|
||||||
success: described(Workspace.Info, "Workspace created"),
|
success: described(Workspace.Info, "Workspace created"),
|
||||||
error: HttpApiError.BadRequest,
|
error: [ApiWorkspaceCreateError, HttpApiError.BadRequest],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "experimental.workspace.create",
|
identifier: "experimental.workspace.create",
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { listAdapters } from "@/control-plane/adapters"
|
|||||||
import { Workspace } from "@/control-plane/workspace"
|
import { Workspace } from "@/control-plane/workspace"
|
||||||
import * as InstanceState from "@/effect/instance-state"
|
import * as InstanceState from "@/effect/instance-state"
|
||||||
import { Vcs } from "@/project/vcs"
|
import { Vcs } from "@/project/vcs"
|
||||||
import { Effect } from "effect"
|
import { Cause, Effect } from "effect"
|
||||||
import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi"
|
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||||
import { InstanceHttpApi } from "../api"
|
import { InstanceHttpApi } from "../api"
|
||||||
import { notFound } from "../errors"
|
import { notFound } from "../errors"
|
||||||
import { ApiVcsApplyError } from "../groups/instance"
|
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) =>
|
export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspace", (handlers) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
@@ -30,7 +30,22 @@ export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspac
|
|||||||
extra: ctx.payload.extra ?? null,
|
extra: ctx.payload.extra ?? null,
|
||||||
projectID: instance.project.id,
|
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* () {
|
const syncList = Effect.fn("WorkspaceHttpApi.syncList")(function* () {
|
||||||
|
|||||||
Reference in New Issue
Block a user