Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down
22 changes: 17 additions & 5 deletions packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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* () {
Expand All @@ -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* () {
Expand Down
Loading