Просмотр исходного кода

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

Signed-off-by: James Murdza <james@jamesmurdza.com>
James Murdza 2 месяцев назад
Родитель
Сommit
9d5f3c1833

+ 13 - 4
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

+ 24 - 13
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<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

+ 17 - 5
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<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({
+        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

+ 11 - 1
packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts

@@ -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",

+ 19 - 4
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* () {