Skip to content
Merged
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
17 changes: 9 additions & 8 deletions packages/opencode/src/control-plane/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { SessionID } from "@/session/schema"
import { errorData } from "@/util/error"
import { waitEvent } from "./util"
import { WorkspaceContext } from "./workspace-context"
import { EffectBridge } from "@/effect/bridge"
import { NonNegativeInt, withStatics } from "@/util/schema"
import { zod as effectZod, zodObject } from "@/util/effect-zod"

Expand Down Expand Up @@ -336,7 +337,7 @@ export const layer = Layer.effect(

const syncWorkspaceLoop = Effect.fn("Workspace.syncWorkspaceLoop")(function* (space: Info) {
const adapter = getAdapter(space.projectID, space.type)
const target = yield* Effect.promise(() => Promise.resolve(adapter.target(space)))
const target = yield* EffectBridge.fromPromise(() => adapter.target(space))

if (target.type === "local") return

Expand Down Expand Up @@ -420,7 +421,7 @@ export const layer = Layer.effect(
if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return

const adapter = getAdapter(space.projectID, space.type)
const target = yield* Effect.promise(() => Promise.resolve(adapter.target(space)))
const target = yield* EffectBridge.fromPromise(() => adapter.target(space))

if (target.type === "local") {
setStatus(space.id, (yield* Effect.promise(() => Filesystem.exists(target.directory))) ? "connected" : "error")
Expand Down Expand Up @@ -459,8 +460,8 @@ export const layer = Layer.effect(
const create = Effect.fn("Workspace.create")(function* (input: CreateInput) {
const id = WorkspaceID.ascending(input.id)
const adapter = getAdapter(input.projectID, input.type)
const config = yield* Effect.promise(() =>
Promise.resolve(adapter.configure({ ...input, id, name: Slug.create(), directory: null })),
const config = yield* EffectBridge.fromPromise(() =>
adapter.configure({ ...input, id, name: Slug.create(), directory: null }),
)

const info: Info = {
Expand Down Expand Up @@ -496,7 +497,7 @@ export const layer = Layer.effect(
OTEL_RESOURCE_ATTRIBUTES: process.env.OTEL_RESOURCE_ATTRIBUTES,
}

yield* Effect.promise(() => adapter.create(config, env))
yield* EffectBridge.fromPromise(() => adapter.create(config, env))
yield* Effect.all(
[
waitEvent({
Expand Down Expand Up @@ -532,7 +533,7 @@ export const layer = Layer.effect(
})

const adapter = getAdapter(space.projectID, space.type)
const target = yield* Effect.promise(() => Promise.resolve(adapter.target(space)))
const target = yield* EffectBridge.fromPromise(() => adapter.target(space))

yield* sync.run(Session.Event.Updated, {
sessionID: input.sessionID,
Expand Down Expand Up @@ -724,10 +725,10 @@ export const layer = Layer.effect(
yield* stopSync(id)

const info = fromRow(row)
yield* Effect.catch(
yield* Effect.catchCause(
Effect.gen(function* () {
const adapter = getAdapter(info.projectID, row.type)
yield* Effect.tryPromise(() => Promise.resolve(adapter.remove(info)))
yield* EffectBridge.fromPromise(() => adapter.remove(info))
}),
() =>
Effect.sync(() => {
Expand Down
19 changes: 19 additions & 0 deletions packages/opencode/src/effect/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,25 @@ function restore<R>(instance: InstanceContext | undefined, workspace: WorkspaceI
return fn()
}

/**
* Bridge from Effect into a Promise-returning JS callback while installing
* legacy `Instance.context` and `WorkspaceContext` AsyncLocalStorage for
* the duration of the callback. Effect's `InstanceRef`/`WorkspaceRef` do
* not propagate across async/await boundaries inside `Effect.promise(() =>
* async fn)` callbacks that re-enter Effect via `AppRuntime.runPromise`,
* but Node's AsyncLocalStorage does. Use this whenever an Effect crosses
* into JS that may itself spawn new Effect runtimes (workspace adapters,
* legacy plugins, etc.).
*
* Mirrors `Effect.promise` but restores legacy ALS first.
*/
export const fromPromise = <T>(fn: () => Promise<T> | T): Effect.Effect<T> =>
Effect.gen(function* () {
const instance = yield* InstanceRef
const workspace = yield* WorkspaceRef
return yield* Effect.promise(() => Promise.resolve(restore(instance, workspace, () => fn())))
})

export function make(): Effect.Effect<Shape> {
return Effect.gen(function* () {
const ctx = yield* Effect.context()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getAdapter } from "@/control-plane/adapters"
import { WorkspaceID } from "@/control-plane/schema"
import type { Target } from "@/control-plane/types"
import { Workspace } from "@/control-plane/workspace"
import { EffectBridge } from "@/effect/bridge"
import { Session } from "@/session/session"
import { HttpApiProxy } from "./proxy"
import * as Fence from "@/server/fence"
Expand Down Expand Up @@ -79,10 +80,8 @@ function missingWorkspaceResponse(id: WorkspaceID): HttpServerResponse.HttpServe
}

function resolveTarget(workspace: Workspace.Info): Effect.Effect<Target> {
return Effect.gen(function* () {
const adapter = yield* Effect.sync(() => getAdapter(workspace.projectID, workspace.type))
return yield* Effect.promise(() => Promise.resolve(adapter.target(workspace)))
})
const adapter = getAdapter(workspace.projectID, workspace.type)
return EffectBridge.fromPromise(() => adapter.target(workspace))
}

function proxyRemote(
Expand Down
18 changes: 18 additions & 0 deletions packages/opencode/test/server/httpapi-workspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,24 @@ describe("workspace HttpApi", () => {
}),
)

it.live("creates a real git worktree workspace via the builtin adapter", () =>
Effect.gen(function* () {
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
const dir = yield* tmpdirScoped({ git: true })

const created = yield* request(WorkspacePaths.list, dir, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ type: "worktree", branch: null }),
})

const body = yield* Effect.promise(() => created.text())
expect({ status: created.status, body }).toMatchObject({ status: 200 })
const workspace = JSON.parse(body) as Workspace.Info
expect(workspace).toMatchObject({ type: "worktree" })
}),
)

it.live("documents legacy Hono accepting the TUI payload shape", () =>
Effect.gen(function* () {
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
Expand Down
Loading