Skip to content
Closed
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
4 changes: 2 additions & 2 deletions packages/opencode/specs/effect/http-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho
| `project` | `bridged` | list, current, git init, update |
| `file` | `bridged` partial | find text/file/symbol, list/content/status |
| `mcp` | `bridged` | status, add, OAuth, connect/disconnect |
| `workspace` | `bridged` | adaptor/list/status/create/remove/session-restore |
| `workspace` | `bridged` | adapter/list/status/create/remove/session-restore |
| top-level instance routes | `bridged` | path, vcs, command, agent, skill, lsp, formatter, dispose |
| experimental JSON routes | `bridged` | console, tool, worktree list/mutations, global session list, resource list |
| `session` | `bridged` | read, lifecycle, prompt, message/part mutations, revert, permission reply |
Expand Down Expand Up @@ -279,7 +279,7 @@ This checklist tracks bridge parity only. Checked routes are available through t

### Workspace Routes

- [x] `GET /experimental/workspace/adaptor` - list workspace adaptors.
- [x] `GET /experimental/workspace/adapter` - list workspace adapters.
- [x] `POST /experimental/workspace` - create workspace.
- [x] `GET /experimental/workspace` - list workspaces.
- [x] `GET /experimental/workspace/status` - workspace status.
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/specs/effect/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ piecewise.
- [ ] `src/cli/cmd/tui/event.ts`
- [ ] `src/cli/ui.ts`
- [ ] `src/command/index.ts`
- [x] `src/control-plane/adaptors/worktree.ts`
- [x] `src/control-plane/adapters/worktree.ts`
- [x] `src/control-plane/types.ts`
- [x] `src/control-plane/workspace.ts`
- [ ] `src/file/index.ts`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
})()
})

Expand All @@ -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",
},
]
}
Expand Down
46 changes: 46 additions & 0 deletions packages/opencode/src/control-plane/adapters/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { lazy } from "@/util/lazy"
import type { ProjectID } from "@/project/schema"
import type { WorkspaceAdapter, WorkspaceAdapterEntry } from "../types"

const BUILTIN: Record<string, () => Promise<WorkspaceAdapter>> = {
worktree: lazy(async () => (await import("./worktree")).WorktreeAdapter),
}

const state = new Map<ProjectID, Map<string, WorkspaceAdapter>>()

export async function getAdapter(projectID: ProjectID, type: string): Promise<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, init]) => {
const adapter = await init()
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)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Schema } from "effect"
import { AppRuntime } from "@/effect/app-runtime"
import { Worktree } from "@/worktree"
import { type WorkspaceAdaptor, WorkspaceInfo } from "../types"
import { type WorkspaceAdapter, WorkspaceInfo } from "../types"

const WorktreeConfig = Schema.Struct({
name: WorkspaceInfo.fields.name,
Expand All @@ -10,7 +10,7 @@ const WorktreeConfig = Schema.Struct({
})
const decodeWorktreeConfig = Schema.decodeUnknownSync(WorktreeConfig)

export const WorktreeAdaptor: WorkspaceAdaptor = {
export const WorktreeAdapter: WorkspaceAdapter = {
name: "Worktree",
description: "Create a git worktree",
async configure(info) {
Expand Down
46 changes: 0 additions & 46 deletions packages/opencode/src/control-plane/adaptors/index.ts

This file was deleted.

6 changes: 3 additions & 3 deletions packages/opencode/src/control-plane/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
| {
Expand All @@ -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>
Expand Down
26 changes: 13 additions & 13 deletions packages/opencode/src/control-plane/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,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 { parseSSE } from "./sse"
Expand Down Expand Up @@ -87,9 +87,9 @@ export type CreateInput = Schema.Schema.Type<typeof CreateInput>

export const create = fn(CreateInput.zod, async (input) => {
const id = WorkspaceID.ascending(input.id)
const adaptor = await getAdaptor(input.projectID, input.type)
const adapter = await getAdapter(input.projectID, input.type)

const config = await adaptor.configure({ ...input, id, name: Slug.create(), directory: null })
const config = await adapter.configure({ ...input, id, name: Slug.create(), directory: null })

const info: Info = {
id,
Expand Down Expand Up @@ -123,7 +123,7 @@ export const create = fn(CreateInput.zod, async (input) => {
OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
OTEL_RESOURCE_ATTRIBUTES: process.env.OTEL_RESOURCE_ATTRIBUTES,
}
await adaptor.create(config, env)
await adapter.create(config, env)

startSync(info)

Expand Down Expand Up @@ -156,8 +156,8 @@ export const sessionRestore = fn(SessionRestoreInput.zod, async (input) => {
const space = await get(input.workspaceID)
if (!space) throw new Error(`Workspace not found: ${input.workspaceID}`)

const adaptor = await getAdaptor(space.projectID, space.type)
const target = await adaptor.target(space)
const adapter = await getAdapter(space.projectID, space.type)
const target = await adapter.target(space)

// Need to switch the workspace of the session
SyncEvent.run(Session.Event.Updated, {
Expand Down Expand Up @@ -329,10 +329,10 @@ export const remove = fn(WorkspaceID.zod, async (id) => {

const info = fromRow(row)
try {
const adaptor = await getAdaptor(info.projectID, row.type)
await adaptor.remove(info)
const adapter = await getAdapter(info.projectID, row.type)
await adapter.remove(info)
} catch {
log.error("adaptor not available when removing workspace", { type: row.type })
log.error("adapter not available when removing workspace", { type: row.type })
}
Database.use((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run())
return info
Expand Down Expand Up @@ -501,8 +501,8 @@ async function syncHistory(space: Info, url: URL | string, headers: HeadersInit
}

async function syncWorkspaceLoop(space: Info, signal: AbortSignal) {
const adaptor = await getAdaptor(space.projectID, space.type)
const target = await adaptor.target(space)
const adapter = await getAdapter(space.projectID, space.type)
const target = await adapter.target(space)

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

Expand Down Expand Up @@ -568,8 +568,8 @@ async function syncWorkspaceLoop(space: Info, signal: AbortSignal) {
async function startSync(space: Info) {
if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return

const adaptor = await getAdaptor(space.projectID, space.type)
const target = await adaptor.target(space)
const adapter = await getAdapter(space.projectID, space.type)
const target = await adapter.target(space)

if (target.type === "local") {
void Filesystem.exists(target.directory).then((exists) => {
Expand Down
10 changes: 5 additions & 5 deletions packages/opencode/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -23,8 +23,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" })

Expand Down Expand Up @@ -136,8 +136,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 {
Expand Down
18 changes: 9 additions & 9 deletions packages/opencode/src/server/routes/control/workspace.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Hono } from "hono"
import { describeRoute, resolver, validator } from "hono-openapi"
import z from "zod"
import { listAdaptors } from "@/control-plane/adaptors"
import { listAdapters } from "@/control-plane/adapters"
import { Workspace } from "@/control-plane/workspace"
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"
Expand All @@ -16,24 +16,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(
Expand Down
Loading
Loading