refactor: rename workspace adapters (#25272)

This commit is contained in:
Kit Langton
2026-05-01 07:36:52 -04:00
committed by GitHub
parent 97ed9ba624
commit 8c79c58c4d
25 changed files with 225 additions and 225 deletions

View File

@@ -10,7 +10,7 @@ import { errorMessage } from "@/util/error"
import { useSDK } from "../context/sdk"
import { useToast } from "../ui/toast"
type Adaptor = {
type Adapter = {
type: string
name: string
description: string
@@ -108,26 +108,26 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
const sdk = useSDK()
const toast = useToast()
const [creating, setCreating] = createSignal<string>()
const [adaptors, setAdaptors] = createSignal<Adaptor[]>()
const [adapters, setAdapters] = createSignal<Adapter[]>()
onMount(() => {
dialog.setSize("medium")
void (async () => {
const dir = sync.path.directory || sdk.directory
const url = new URL("/experimental/workspace/adaptor", sdk.url)
const url = new URL("/experimental/workspace/adapter", sdk.url)
if (dir) url.searchParams.set("directory", dir)
const res = await sdk
.fetch(url)
.then((x) => x.json() as Promise<Adaptor[]>)
.then((x) => x.json() as Promise<Adapter[]>)
.catch(() => undefined)
if (!res) {
toast.show({
message: "Failed to load workspace adaptors",
message: "Failed to load workspace adapters",
variant: "error",
})
return
}
setAdaptors(res)
setAdapters(res)
})()
})
@@ -142,13 +142,13 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
},
]
}
const list = adaptors()
const list = adapters()
if (!list) {
return [
{
title: "Loading workspaces...",
value: "loading" as const,
description: "Fetching available workspace adaptors",
description: "Fetching available workspace adapters",
},
]
}

View File

@@ -0,0 +1,45 @@
import type { ProjectID } from "@/project/schema"
import type { WorkspaceAdapter, WorkspaceAdapterEntry } from "../types"
import { WorktreeAdapter } from "./worktree"
const BUILTIN: Record<string, WorkspaceAdapter> = {
worktree: WorktreeAdapter,
}
const state = new Map<ProjectID, Map<string, WorkspaceAdapter>>()
export function getAdapter(projectID: ProjectID, type: string): WorkspaceAdapter {
const custom = state.get(projectID)?.get(type)
if (custom) return custom
const builtin = BUILTIN[type]
if (builtin) return builtin
throw new Error(`Unknown workspace adapter: ${type}`)
}
export async function listAdapters(projectID: ProjectID): Promise<WorkspaceAdapterEntry[]> {
const builtin = await Promise.all(
Object.entries(BUILTIN).map(async ([type, adapter]) => {
return {
type,
name: adapter.name,
description: adapter.description,
}
}),
)
const custom = [...(state.get(projectID)?.entries() ?? [])].map(([type, adapter]) => ({
type,
name: adapter.name,
description: adapter.description,
}))
return [...builtin, ...custom]
}
// Plugins can be loaded per-project so we need to scope them. If you
// want to install a global one pass `ProjectID.global`
export function registerAdapter(projectID: ProjectID, type: string, adapter: WorkspaceAdapter) {
const adapters = state.get(projectID) ?? new Map<string, WorkspaceAdapter>()
adapters.set(type, adapter)
state.set(projectID, adapters)
}

View File

@@ -1,5 +1,5 @@
import { Schema } from "effect"
import { type WorkspaceAdaptor, WorkspaceInfo } from "../types"
import { type WorkspaceAdapter, WorkspaceInfo } from "../types"
const WorktreeConfig = Schema.Struct({
name: WorkspaceInfo.fields.name,
@@ -13,7 +13,7 @@ async function loadWorktree() {
return { AppRuntime, Worktree }
}
export const WorktreeAdaptor: WorkspaceAdaptor = {
export const WorktreeAdapter: WorkspaceAdapter = {
name: "Worktree",
description: "Create a git worktree",
async configure(info) {

View File

@@ -1,45 +0,0 @@
import type { ProjectID } from "@/project/schema"
import type { WorkspaceAdaptor, WorkspaceAdaptorEntry } from "../types"
import { WorktreeAdaptor } from "./worktree"
const BUILTIN: Record<string, WorkspaceAdaptor> = {
worktree: WorktreeAdaptor,
}
const state = new Map<ProjectID, Map<string, WorkspaceAdaptor>>()
export function getAdaptor(projectID: ProjectID, type: string): WorkspaceAdaptor {
const custom = state.get(projectID)?.get(type)
if (custom) return custom
const builtin = BUILTIN[type]
if (builtin) return builtin
throw new Error(`Unknown workspace adaptor: ${type}`)
}
export async function listAdaptors(projectID: ProjectID): Promise<WorkspaceAdaptorEntry[]> {
const builtin = await Promise.all(
Object.entries(BUILTIN).map(async ([type, adaptor]) => {
return {
type,
name: adaptor.name,
description: adaptor.description,
}
}),
)
const custom = [...(state.get(projectID)?.entries() ?? [])].map(([type, adaptor]) => ({
type,
name: adaptor.name,
description: adaptor.description,
}))
return [...builtin, ...custom]
}
// Plugins can be loaded per-project so we need to scope them. If you
// want to install a global one pass `ProjectID.global`
export function registerAdaptor(projectID: ProjectID, type: string, adaptor: WorkspaceAdaptor) {
const adaptors = state.get(projectID) ?? new Map<string, WorkspaceAdaptor>()
adaptors.set(type, adaptor)
state.set(projectID, adaptors)
}

View File

@@ -17,12 +17,12 @@ export const WorkspaceInfo = Schema.Struct({
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type WorkspaceInfo = DeepMutable<Schema.Schema.Type<typeof WorkspaceInfo>>
export const WorkspaceAdaptorEntry = Schema.Struct({
export const WorkspaceAdapterEntry = Schema.Struct({
type: Schema.String,
name: Schema.String,
description: Schema.String,
}).pipe(withStatics((s) => ({ zod: zod(s) })))
export type WorkspaceAdaptorEntry = Schema.Schema.Type<typeof WorkspaceAdaptorEntry>
export type WorkspaceAdapterEntry = Schema.Schema.Type<typeof WorkspaceAdapterEntry>
export type Target =
| {
@@ -35,7 +35,7 @@ export type Target =
headers?: HeadersInit
}
export type WorkspaceAdaptor = {
export type WorkspaceAdapter = {
name: string
description: string
configure(info: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo>

View File

@@ -16,7 +16,7 @@ import { Filesystem } from "@/util/filesystem"
import { ProjectID } from "@/project/schema"
import { Slug } from "@opencode-ai/core/util/slug"
import { WorkspaceTable } from "./workspace.sql"
import { getAdaptor } from "./adaptors"
import { getAdapter } from "./adapters"
import { type WorkspaceInfo, WorkspaceInfo as WorkspaceInfoSchema } from "./types"
import { WorkspaceID } from "./schema"
import { Session } from "@/session/session"
@@ -335,8 +335,8 @@ export const layer = Layer.effect(
})
const syncWorkspaceLoop = Effect.fn("Workspace.syncWorkspaceLoop")(function* (space: Info) {
const adaptor = getAdaptor(space.projectID, space.type)
const target = yield* Effect.promise(() => Promise.resolve(adaptor.target(space)))
const adapter = getAdapter(space.projectID, space.type)
const target = yield* Effect.promise(() => Promise.resolve(adapter.target(space)))
if (target.type === "local") return
@@ -419,8 +419,8 @@ export const layer = Layer.effect(
const startSync = Effect.fn("Workspace.startSync")(function* (space: Info) {
if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return
const adaptor = getAdaptor(space.projectID, space.type)
const target = yield* Effect.promise(() => Promise.resolve(adaptor.target(space)))
const adapter = getAdapter(space.projectID, space.type)
const target = yield* Effect.promise(() => Promise.resolve(adapter.target(space)))
if (target.type === "local") {
setStatus(space.id, (yield* Effect.promise(() => Filesystem.exists(target.directory))) ? "connected" : "error")
@@ -458,9 +458,9 @@ export const layer = Layer.effect(
const create = Effect.fn("Workspace.create")(function* (input: CreateInput) {
const id = WorkspaceID.ascending(input.id)
const adaptor = getAdaptor(input.projectID, input.type)
const adapter = getAdapter(input.projectID, input.type)
const config = yield* Effect.promise(() =>
Promise.resolve(adaptor.configure({ ...input, id, name: Slug.create(), directory: null })),
Promise.resolve(adapter.configure({ ...input, id, name: Slug.create(), directory: null })),
)
const info: Info = {
@@ -496,7 +496,7 @@ export const layer = Layer.effect(
OTEL_RESOURCE_ATTRIBUTES: process.env.OTEL_RESOURCE_ATTRIBUTES,
}
yield* Effect.promise(() => adaptor.create(config, env))
yield* Effect.promise(() => adapter.create(config, env))
yield* Effect.all(
[
waitEvent({
@@ -531,8 +531,8 @@ export const layer = Layer.effect(
workspaceID: input.workspaceID,
})
const adaptor = getAdaptor(space.projectID, space.type)
const target = yield* Effect.promise(() => Promise.resolve(adaptor.target(space)))
const adapter = getAdapter(space.projectID, space.type)
const target = yield* Effect.promise(() => Promise.resolve(adapter.target(space)))
yield* sync.run(Session.Event.Updated, {
sessionID: input.sessionID,
@@ -726,12 +726,12 @@ export const layer = Layer.effect(
const info = fromRow(row)
yield* Effect.catch(
Effect.gen(function* () {
const adaptor = getAdaptor(info.projectID, row.type)
yield* Effect.tryPromise(() => Promise.resolve(adaptor.remove(info)))
const adapter = getAdapter(info.projectID, row.type)
yield* Effect.tryPromise(() => Promise.resolve(adapter.remove(info)))
}),
() =>
Effect.sync(() => {
log.error("adaptor not available when removing workspace", { type: row.type })
log.error("adapter not available when removing workspace", { type: row.type })
}),
)

View File

@@ -3,7 +3,7 @@ import type {
PluginInput,
Plugin as PluginInstance,
PluginModule,
WorkspaceAdaptor as PluginWorkspaceAdaptor,
WorkspaceAdapter as PluginWorkspaceAdapter,
} from "@opencode-ai/plugin"
import { Config } from "@/config/config"
import { Bus } from "../bus"
@@ -24,8 +24,8 @@ import { InstanceState } from "@/effect/instance-state"
import { errorMessage } from "@/util/error"
import { PluginLoader } from "./loader"
import { parsePluginSpecifier, readPluginId, readV1Plugin, resolvePluginId } from "./shared"
import { registerAdaptor } from "@/control-plane/adaptors"
import type { WorkspaceAdaptor } from "@/control-plane/types"
import { registerAdapter } from "@/control-plane/adapters"
import type { WorkspaceAdapter } from "@/control-plane/types"
const log = Log.create({ service: "plugin" })
@@ -138,8 +138,8 @@ export const layer = Layer.effect(
worktree: ctx.worktree,
directory: ctx.directory,
experimental_workspace: {
register(type: string, adaptor: PluginWorkspaceAdaptor) {
registerAdaptor(ctx.project.id, type, adaptor as WorkspaceAdaptor)
register(type: string, adapter: PluginWorkspaceAdapter) {
registerAdapter(ctx.project.id, type, adapter as WorkspaceAdapter)
},
},
get serverUrl(): URL {

View File

@@ -2,10 +2,10 @@ import { Hono } from "hono"
import { describeRoute, resolver, validator } from "hono-openapi"
import z from "zod"
import { Effect } from "effect"
import { listAdaptors } from "@/control-plane/adaptors"
import { listAdapters } from "@/control-plane/adapters"
import { Workspace } from "@/control-plane/workspace"
import { AppRuntime } from "@/effect/app-runtime"
import { WorkspaceAdaptorEntry } from "@/control-plane/types"
import { WorkspaceAdapterEntry } from "@/control-plane/types"
import { zodObject } from "@/util/effect-zod"
import { Instance } from "@/project/instance"
import { errors } from "../../error"
@@ -18,24 +18,24 @@ const log = Log.create({ service: "server.workspace" })
export const WorkspaceRoutes = lazy(() =>
new Hono()
.get(
"/adaptor",
"/adapter",
describeRoute({
summary: "List workspace adaptors",
description: "List all available workspace adaptors for the current project.",
operationId: "experimental.workspace.adaptor.list",
summary: "List workspace adapters",
description: "List all available workspace adapters for the current project.",
operationId: "experimental.workspace.adapter.list",
responses: {
200: {
description: "Workspace adaptors",
description: "Workspace adapters",
content: {
"application/json": {
schema: resolver(z.array(zodObject(WorkspaceAdaptorEntry))),
schema: resolver(z.array(zodObject(WorkspaceAdapterEntry))),
},
},
},
},
}),
async (c) => {
return c.json(await listAdaptors(Instance.project.id))
return c.json(await listAdapters(Instance.project.id))
},
)
.post(

View File

@@ -1,5 +1,5 @@
import { Workspace } from "@/control-plane/workspace"
import { WorkspaceAdaptorEntry } from "@/control-plane/types"
import { WorkspaceAdapterEntry } from "@/control-plane/types"
import { NonNegativeInt } from "@/util/schema"
import { Schema, Struct } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
@@ -16,7 +16,7 @@ export const SessionRestoreResponse = Schema.Struct({
})
export const WorkspacePaths = {
adaptors: `${root}/adaptor`,
adapters: `${root}/adapter`,
list: root,
status: `${root}/status`,
remove: `${root}/:id`,
@@ -27,13 +27,13 @@ export const WorkspaceApi = HttpApi.make("workspace")
.add(
HttpApiGroup.make("workspace")
.add(
HttpApiEndpoint.get("adaptors", WorkspacePaths.adaptors, {
success: described(Schema.Array(WorkspaceAdaptorEntry), "Workspace adaptors"),
HttpApiEndpoint.get("adapters", WorkspacePaths.adapters, {
success: described(Schema.Array(WorkspaceAdapterEntry), "Workspace adapters"),
}).annotateMerge(
OpenApi.annotations({
identifier: "experimental.workspace.adaptor.list",
summary: "List workspace adaptors",
description: "List all available workspace adaptors for the current project.",
identifier: "experimental.workspace.adapter.list",
summary: "List workspace adapters",
description: "List all available workspace adapters for the current project.",
}),
),
HttpApiEndpoint.get("list", WorkspacePaths.list, {

View File

@@ -1,4 +1,4 @@
import { listAdaptors } from "@/control-plane/adaptors"
import { listAdapters } from "@/control-plane/adapters"
import { Workspace } from "@/control-plane/workspace"
import * as InstanceState from "@/effect/instance-state"
import { Effect } from "effect"
@@ -10,9 +10,9 @@ export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspac
Effect.gen(function* () {
const workspace = yield* Workspace.Service
const adaptors = Effect.fn("WorkspaceHttpApi.adaptors")(function* () {
const adapters = Effect.fn("WorkspaceHttpApi.adapters")(function* () {
const instance = yield* InstanceState.context
return yield* Effect.promise(() => listAdaptors(instance.project.id))
return yield* Effect.promise(() => listAdapters(instance.project.id))
})
const list = Effect.fn("WorkspaceHttpApi.list")(function* () {
@@ -51,7 +51,7 @@ export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspac
})
return handlers
.handle("adaptors", adaptors)
.handle("adapters", adapters)
.handle("list", list)
.handle("create", create)
.handle("status", status)

View File

@@ -1,4 +1,4 @@
import { getAdaptor } from "@/control-plane/adaptors"
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"
@@ -89,8 +89,8 @@ function missingWorkspaceResponse(id: WorkspaceID): HttpServerResponse.HttpServe
function resolveTarget(workspace: Workspace.Info): Effect.Effect<Target> {
return Effect.gen(function* () {
const adaptor = yield* Effect.sync(() => getAdaptor(workspace.projectID, workspace.type))
return yield* Effect.promise(() => Promise.resolve(adaptor.target(workspace)))
const adapter = yield* Effect.sync(() => getAdapter(workspace.projectID, workspace.type))
return yield* Effect.promise(() => Promise.resolve(adapter.target(workspace)))
})
}

View File

@@ -1,6 +1,6 @@
import type { MiddlewareHandler } from "hono"
import type { UpgradeWebSocket } from "hono/ws"
import { getAdaptor } from "@/control-plane/adaptors"
import { getAdapter } from "@/control-plane/adapters"
import { WorkspaceID } from "@/control-plane/schema"
import { WorkspaceContext } from "@/control-plane/workspace-context"
import { Workspace } from "@/control-plane/workspace"
@@ -91,8 +91,8 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
return next()
}
const adaptor = getAdaptor(workspace.projectID, workspace.type)
const target = await adaptor.target(workspace)
const adapter = getAdapter(workspace.projectID, workspace.type)
const target = await adapter.target(workspace)
if (target.type === "local") {
return WorkspaceContext.provide({