feat(core): add command registry (#30624)

This commit is contained in:
Dax
2026-06-04 02:57:43 -04:00
committed by GitHub
parent 70bb710715
commit 1ff19103a2
150 changed files with 4753 additions and 2657 deletions
@@ -20,7 +20,7 @@ import { SessionApi } from "./groups/session"
import { SyncApi } from "./groups/sync"
import { TuiApi } from "./groups/tui"
import { WorkspaceApi } from "./groups/workspace"
import { V2Api } from "./groups/v2"
import { V2Api } from "@opencode-ai/server/api"
// GlobalEventSchema snapshots the registry after event-producing groups register their variants.
import { GlobalApi } from "./groups/global"
import { Authorization } from "./middleware/authorization"
@@ -60,7 +60,6 @@ export const InstanceHttpApi = HttpApi.make("opencode-instance")
.addHttpApi(ProviderApi)
.addHttpApi(SessionApi)
.addHttpApi(SyncApi)
.addHttpApi(V2Api)
.addHttpApi(TuiApi)
.addHttpApi(WorkspaceApi)
.middleware(SchemaErrorMiddleware)
@@ -69,6 +68,7 @@ export const OpenCodeHttpApi = HttpApi.make("opencode")
.addHttpApi(RootHttpApi)
.addHttpApi(EventApi)
.addHttpApi(InstanceHttpApi)
.addHttpApi(V2Api)
.addHttpApi(PtyConnectApi)
.annotate(HttpApi.AdditionalSchemas, [EventSchema, Question.Replied, Question.Rejected])
@@ -16,6 +16,7 @@ import {
import { described } from "./metadata"
import { QueryBoolean } from "./query"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
const ConsoleStateResponse = Schema.Struct({
consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)),
@@ -51,7 +52,7 @@ const ToolList = Schema.Array(ToolListItem).annotate({ identifier: "ToolList" })
export const ToolListQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
provider: ProviderV2.ID,
model: ProviderV2.ModelID,
model: ModelV2.ID,
})
const WorktreeList = Schema.Array(Schema.String)
@@ -20,11 +20,14 @@ const SyncEventSchemas = EventV2.registry
return [
Schema.Struct({
type: Schema.Literal("sync"),
name: Schema.Literal(EventV2.versionedType(definition.type, definition.sync.version)),
id: Schema.String,
seq: Schema.Finite,
aggregateID: Schema.Literal(definition.sync.aggregate),
data: definition.data,
syncEvent: Schema.Struct({
type: Schema.Literal(EventV2.versionedType(definition.type, definition.sync.version)),
id: Schema.String,
seq: Schema.Finite,
aggregateID: Schema.String,
data: definition.data,
}),
}).annotate({ identifier: `SyncEvent.${definition.type}` }),
]
})
@@ -24,6 +24,7 @@ import { ApiNotFoundError, PermissionNotFoundError, SessionBusyError } from "../
import { described } from "./metadata"
import { QueryBoolean } from "./query"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
const root = "/session"
export const ListQuery = Schema.Struct({
@@ -57,13 +58,13 @@ export const UpdatePayload = Schema.Struct({
})
export const ForkPayload = Schema.Struct(Struct.omit(Session.ForkInput.fields, ["sessionID"]))
export const InitPayload = Schema.Struct({
modelID: ProviderV2.ModelID,
modelID: ModelV2.ID,
providerID: ProviderV2.ID,
messageID: MessageID,
})
export const SummarizePayload = Schema.Struct({
providerID: ProviderV2.ID,
modelID: ProviderV2.ModelID,
modelID: ModelV2.ID,
auto: Schema.optional(Schema.Boolean),
})
export const PromptPayload = Schema.Struct(Struct.omit(SessionPrompt.PromptInput.fields, ["sessionID"]))
@@ -1,27 +0,0 @@
import { HttpApi, OpenApi } from "effect/unstable/httpapi"
import { MessageGroup } from "./v2/message"
import { ModelGroup } from "./v2/model"
import { ProviderGroup } from "./v2/provider"
import { SessionGroup } from "./v2/session"
import { PermissionGroup, PermissionSavedGroup, SessionPermissionGroup } from "./v2/permission"
import { FileSystemGroup } from "./v2/fs"
import { QuestionGroup, SessionQuestionGroup } from "./v2/question"
export const V2Api = HttpApi.make("v2")
.add(SessionGroup)
.add(MessageGroup)
.add(ModelGroup)
.add(ProviderGroup)
.add(PermissionGroup)
.add(SessionPermissionGroup)
.add(PermissionSavedGroup)
.add(FileSystemGroup)
.add(QuestionGroup)
.add(SessionQuestionGroup)
.annotateMerge(
OpenApi.annotations({
title: "opencode experimental HttpApi",
version: "0.0.1",
description: "Experimental HttpApi surface for selected instance routes.",
}),
)
@@ -1,56 +0,0 @@
import { FileSystem } from "@opencode-ai/core/filesystem"
import { RelativePath } from "@opencode-ai/core/schema"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { V2Authorization } from "../../middleware/authorization"
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
const ReadQuery = Schema.Struct({
...LocationQuery.fields,
path: RelativePath,
reference: Schema.String.pipe(Schema.optional),
})
const ListQuery = Schema.Struct({
...LocationQuery.fields,
path: RelativePath.pipe(Schema.optional),
reference: Schema.String.pipe(Schema.optional),
})
export const FileSystemGroup = HttpApiGroup.make("v2.fs")
.add(
HttpApiEndpoint.get("read", "/api/fs/read", {
query: ReadQuery,
success: FileSystem.Content,
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.fs.read",
summary: "Read file",
description: "Read one file relative to the requested location.",
}),
),
)
.add(
HttpApiEndpoint.get("list", "/api/fs/list", {
query: ListQuery,
success: Schema.Array(FileSystem.Entry),
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.fs.list",
summary: "List directory",
description: "List direct children of one directory relative to the requested location.",
}),
),
)
.annotateMerge(
OpenApi.annotations({
title: "v2 filesystem",
description: "Experimental v2 location-scoped filesystem routes.",
}),
)
.middleware(V2LocationMiddleware)
.middleware(V2Authorization)
@@ -1,74 +0,0 @@
import { Catalog } from "@opencode-ai/core/catalog"
import { Location } from "@opencode-ai/core/location"
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { FileSystem } from "@opencode-ai/core/filesystem"
import { PermissionV2 } from "@opencode-ai/core/permission"
import { ProjectReference } from "@opencode-ai/core/project-reference"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
import { QuestionV2 } from "@opencode-ai/core/question"
import { Effect, Layer, Schema } from "effect"
import { HttpServerRequest } from "effect/unstable/http"
import { HttpApiMiddleware, OpenApi } from "effect/unstable/httpapi"
export const LocationQuery = Schema.Struct({
location: Schema.optional(
Schema.Struct({
directory: Schema.optional(Schema.String),
workspace: Schema.optional(Schema.String),
}),
),
}).annotate({ identifier: "V2LocationQuery" })
export const locationQueryOpenApi = OpenApi.annotations({
transform: (operation) => {
const parameters = operation.parameters
if (!Array.isArray(parameters)) return operation
return {
...operation,
parameters: parameters.map((parameter) =>
parameter?.name === "location" && parameter?.in === "query"
? { ...parameter, style: "deepObject", explode: true }
: parameter,
),
}
},
})
export class V2LocationMiddleware extends HttpApiMiddleware.Service<
V2LocationMiddleware,
{
provides:
| Catalog.Service
| PluginBoot.Service
| PermissionV2.Service
| ProjectReference.Service
| FileSystem.Service
| QuestionV2.Service
}
>()("@opencode/ExperimentalHttpApiV2Location") {}
function ref(request: HttpServerRequest.HttpServerRequest): Location.Ref {
const query = new URL(request.url, "http://localhost").searchParams
const workspaceID = query.get("location[workspace]") || request.headers["x-opencode-workspace"]
return {
directory: AbsolutePath.make(
query.get("location[directory]") || request.headers["x-opencode-directory"] || process.cwd(),
),
workspaceID: workspaceID ? WorkspaceV2.ID.make(workspaceID) : undefined,
}
}
export const layer = Layer.effect(
V2LocationMiddleware,
Effect.gen(function* () {
const locations = yield* LocationServiceMap
return V2LocationMiddleware.of((effect) =>
Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest
return yield* effect.pipe(Effect.provide(locations.get(ref(request))))
}),
)
}),
)
@@ -1,55 +0,0 @@
import { SessionID } from "@/session/schema"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { InvalidCursorError, SessionNotFoundError, UnknownError } from "../../errors"
import { V2Authorization } from "../../middleware/authorization"
import { WorkspaceRoutingQueryFields } from "../../middleware/workspace-routing"
export const MessagesQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
limit: Schema.optional(
Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(200)),
).annotate({
description: "Maximum number of messages to return. When omitted, the endpoint returns its default page size.",
}),
order: Schema.optional(Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")])).annotate({
description: "Message order for the first page. Use desc for newest first or asc for oldest first.",
}),
cursor: Schema.optional(
Schema.String.annotate({
description:
"Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order.",
}),
),
}).annotate({ identifier: "V2SessionMessagesQuery" })
export const MessageGroup = HttpApiGroup.make("v2.message")
.add(
HttpApiEndpoint.get("messages", "/api/session/:sessionID/message", {
params: { sessionID: SessionID },
query: MessagesQuery,
success: Schema.Struct({
items: Schema.Array(SessionMessage.Message),
cursor: Schema.Struct({
previous: Schema.String.pipe(Schema.optional),
next: Schema.String.pipe(Schema.optional),
}),
}).annotate({ identifier: "V2SessionMessagesResponse" }),
error: [InvalidCursorError, SessionNotFoundError, UnknownError],
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.session.messages",
summary: "Get v2 session messages",
description:
"Retrieve projected v2 messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline.",
}),
),
)
.annotateMerge(
OpenApi.annotations({
title: "v2 messages",
description: "Experimental v2 message routes.",
}),
)
.middleware(V2Authorization)
@@ -1,31 +0,0 @@
import { ModelV2 } from "@opencode-ai/core/model"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { ServiceUnavailableError } from "../../errors"
import { V2Authorization } from "../../middleware/authorization"
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
export const ModelGroup = HttpApiGroup.make("v2.model")
.add(
HttpApiEndpoint.get("models", "/api/model", {
query: LocationQuery,
success: Schema.Array(ModelV2.PublicInfo),
error: ServiceUnavailableError,
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.model.list",
summary: "List v2 models",
description: "Retrieve available v2 models ordered by release date.",
}),
),
)
.annotateMerge(
OpenApi.annotations({
title: "v2 models",
description: "Experimental v2 model routes.",
}),
)
.middleware(V2LocationMiddleware)
.middleware(V2Authorization)
@@ -1,94 +0,0 @@
import { PermissionV2 } from "@opencode-ai/core/permission"
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
import { ProjectV2 } from "@opencode-ai/core/project"
import { SessionV2 } from "@opencode-ai/core/session"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { PermissionNotFoundError, SessionNotFoundError } from "../../errors"
import { V2Authorization } from "../../middleware/authorization"
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
export const PermissionGroup = HttpApiGroup.make("v2.permission")
.add(
HttpApiEndpoint.get("permissionRequests", "/api/permission/request", {
query: LocationQuery,
success: Schema.Array(PermissionV2.Request),
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.permission.request.list",
summary: "List pending permission requests",
description: "Retrieve pending permission requests for a location.",
}),
),
)
.annotateMerge(OpenApi.annotations({ title: "v2 permissions", description: "Experimental v2 permission routes." }))
.middleware(V2LocationMiddleware)
.middleware(V2Authorization)
export const SessionPermissionGroup = HttpApiGroup.make("v2.session.permission")
.add(
HttpApiEndpoint.get("sessionPermissionRequests", "/api/session/:sessionID/permission/request", {
params: { sessionID: SessionV2.ID },
success: Schema.Array(PermissionV2.Request),
error: SessionNotFoundError,
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.session.permission.list",
summary: "List session permission requests",
description: "Retrieve pending permission requests owned by a session.",
}),
),
)
.add(
HttpApiEndpoint.post("permissionRequestReply", "/api/session/:sessionID/permission/request/:requestID/reply", {
params: { sessionID: SessionV2.ID, requestID: PermissionV2.ID },
payload: Schema.Struct({
reply: PermissionV2.Reply,
message: Schema.String.pipe(Schema.optional),
}),
success: HttpApiSchema.NoContent,
error: [SessionNotFoundError, PermissionNotFoundError],
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.session.permission.reply",
summary: "Reply to pending permission request",
description: "Respond to a pending permission request owned by a session.",
}),
),
)
.annotateMerge(
OpenApi.annotations({ title: "v2 session permissions", description: "Experimental v2 session permission routes." }),
)
.middleware(V2Authorization)
export const PermissionSavedGroup = HttpApiGroup.make("v2.permission.saved")
.add(
HttpApiEndpoint.get("savedPermissions", "/api/permission/saved", {
query: Schema.Struct({ projectID: ProjectV2.ID.pipe(Schema.optional) }),
success: Schema.Array(PermissionSaved.Info),
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.permission.saved.list",
summary: "List saved permissions",
description: "Retrieve saved permissions, optionally filtered by project.",
}),
),
)
.add(
HttpApiEndpoint.delete("removeSavedPermission", "/api/permission/saved/:id", {
params: { id: PermissionSaved.ID },
success: HttpApiSchema.NoContent,
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.permission.saved.remove",
summary: "Remove saved permission",
description: "Remove a saved permission by ID.",
}),
),
)
.annotateMerge(
OpenApi.annotations({ title: "v2 saved permissions", description: "Experimental v2 saved permission routes." }),
)
.middleware(V2Authorization)
@@ -1,48 +0,0 @@
import { ProviderV2 } from "@opencode-ai/core/provider"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { ProviderNotFoundError, ServiceUnavailableError } from "../../errors"
import { V2Authorization } from "../../middleware/authorization"
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
export const ProviderGroup = HttpApiGroup.make("v2.provider")
.add(
HttpApiEndpoint.get("providers", "/api/provider", {
query: LocationQuery,
success: Schema.Array(ProviderV2.PublicInfo),
error: ServiceUnavailableError,
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.provider.list",
summary: "List v2 providers",
description: "Retrieve active v2 AI providers so clients can show provider availability and configuration.",
}),
),
)
.add(
HttpApiEndpoint.get("provider", "/api/provider/:providerID", {
params: { providerID: ProviderV2.ID },
query: LocationQuery,
success: ProviderV2.PublicInfo,
error: [ProviderNotFoundError, ServiceUnavailableError],
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.provider.get",
summary: "Get v2 provider",
description:
"Retrieve a single v2 AI provider so clients can inspect its availability and endpoint settings.",
}),
),
)
.annotateMerge(
OpenApi.annotations({
title: "v2 providers",
description: "Experimental v2 provider routes.",
}),
)
.middleware(V2LocationMiddleware)
.middleware(V2Authorization)
@@ -1,59 +0,0 @@
import { QuestionV2 } from "@opencode-ai/core/question"
import { SessionV2 } from "@opencode-ai/core/session"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { QuestionNotFoundError, SessionNotFoundError } from "../../errors"
import { V2Authorization } from "../../middleware/authorization"
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
export const QuestionGroup = HttpApiGroup.make("v2.question")
.add(
HttpApiEndpoint.get("questionRequests", "/api/question/request", {
query: LocationQuery,
success: Schema.Array(QuestionV2.Request),
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.question.request.list",
summary: "List pending question requests",
description: "Retrieve pending question requests for a location.",
}),
),
)
.annotateMerge(OpenApi.annotations({ title: "v2 questions", description: "Experimental v2 question routes." }))
.middleware(V2LocationMiddleware)
.middleware(V2Authorization)
export const SessionQuestionGroup = HttpApiGroup.make("v2.session.question")
.add(
HttpApiEndpoint.post("questionRequestReply", "/api/session/:sessionID/question/request/:requestID/reply", {
params: { sessionID: SessionV2.ID, requestID: QuestionV2.ID },
payload: QuestionV2.Reply,
success: HttpApiSchema.NoContent,
error: [SessionNotFoundError, QuestionNotFoundError],
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.session.question.reply",
summary: "Reply to pending question request",
description: "Answer a pending question request owned by a session.",
}),
),
)
.add(
HttpApiEndpoint.post("questionRequestReject", "/api/session/:sessionID/question/request/:requestID/reject", {
params: { sessionID: SessionV2.ID, requestID: QuestionV2.ID },
success: HttpApiSchema.NoContent,
error: [SessionNotFoundError, QuestionNotFoundError],
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.session.question.reject",
summary: "Reject pending question request",
description: "Reject a pending question request owned by a session.",
}),
),
)
.annotateMerge(
OpenApi.annotations({ title: "v2 session questions", description: "Experimental v2 session question routes." }),
)
.middleware(V2Authorization)
@@ -1,178 +0,0 @@
import { SessionID } from "@/session/schema"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { SessionInput } from "@opencode-ai/core/session/input"
import { Prompt } from "@opencode-ai/core/session/prompt"
import { SessionV2 } from "@opencode-ai/core/session"
import { ProjectV2 } from "@opencode-ai/core/project"
import { AbsolutePath, PositiveInt, RelativePath, withStatics } from "@opencode-ai/core/schema"
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
import { Schema, Struct } from "effect"
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import {
ConflictError,
InvalidCursorError,
InvalidRequestError,
ServiceUnavailableError,
SessionNotFoundError,
UnknownError,
} from "../../errors"
import { V2Authorization } from "../../middleware/authorization"
import { WorkspaceRoutingQuery } from "../../middleware/workspace-routing"
const SessionsQueryFields = {
workspace: WorkspaceV2.ID.pipe(Schema.optional),
limit: Schema.NumberFromString.pipe(Schema.decodeTo(PositiveInt), Schema.optional).annotate({
description: "Maximum number of sessions to return. Defaults to the newest 50 sessions.",
}),
order: Schema.optional(Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")])).annotate({
description: "Session order for the first page. Use desc for newest first or asc for oldest first.",
}),
search: Schema.optional(Schema.String),
}
const SessionsDirectoryQuery = Schema.Struct({
...SessionsQueryFields,
directory: AbsolutePath,
})
const SessionsProjectQuery = Schema.Struct({
...SessionsQueryFields,
project: ProjectV2.ID,
subpath: RelativePath.pipe(Schema.optional),
})
const SessionsAllQuery = Schema.Struct(SessionsQueryFields)
const withCursor = <Fields extends Schema.Struct.Fields>(schema: Schema.Struct<Fields>) =>
schema.mapFields((fields) => ({
...Struct.omit(fields, ["limit"]),
anchor: SessionV2.ListAnchor,
}))
const SessionsCursorInput = Schema.Union([
withCursor(SessionsDirectoryQuery),
withCursor(SessionsProjectQuery),
withCursor(SessionsAllQuery),
])
const SessionsCursorJson = Schema.fromJsonString(SessionsCursorInput)
const encodeSessionsCursor = Schema.encodeSync(SessionsCursorJson)
const decodeSessionsCursor = Schema.decodeUnknownEffect(SessionsCursorJson)
export const SessionsCursor = Schema.String.pipe(
Schema.brand("V2SessionsCursor"),
withStatics((schema) => {
const make = schema.make
return {
make: (input: typeof SessionsCursorInput.Type) =>
make(Buffer.from(encodeSessionsCursor(input)).toString("base64url")),
parse: (input: string) => decodeSessionsCursor(Buffer.from(input, "base64url").toString("utf8")),
}
}),
)
export type SessionsCursor = typeof SessionsCursor.Type
const SessionsCursorQuery = Schema.Struct({
cursor: SessionsCursor.annotate({
description: "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response.",
}),
limit: SessionsQueryFields.limit,
})
export const SessionsQuery = Schema.Struct({
...SessionsQueryFields,
directory: AbsolutePath.pipe(Schema.optional),
project: ProjectV2.ID.pipe(Schema.optional),
subpath: RelativePath.pipe(Schema.optional),
cursor: SessionsCursorQuery.fields.cursor.pipe(Schema.optional),
}).annotate({ identifier: "V2SessionsQuery" })
export const SessionGroup = HttpApiGroup.make("v2.session")
.add(
HttpApiEndpoint.get("sessions", "/api/session", {
query: SessionsQuery,
success: Schema.Struct({
items: Schema.Array(SessionV2.Info),
cursor: Schema.Struct({
previous: SessionsCursor.pipe(Schema.optional),
next: SessionsCursor.pipe(Schema.optional),
}),
}).annotate({ identifier: "V2SessionsResponse" }),
error: [InvalidCursorError, InvalidRequestError],
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.session.list",
summary: "List v2 sessions",
description:
"Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list.",
}),
),
)
.add(
HttpApiEndpoint.post("prompt", "/api/session/:sessionID/prompt", {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
payload: Schema.Struct({
id: SessionMessage.ID.pipe(Schema.optional),
prompt: Prompt,
delivery: SessionInput.Delivery.pipe(Schema.optional),
resume: Schema.Boolean.pipe(Schema.optional),
}),
success: SessionMessage.User,
error: [ConflictError, SessionNotFoundError],
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.session.prompt",
summary: "Send v2 message",
description: "Durably admit one v2 session input and schedule agent-loop execution unless resume is false.",
}),
),
)
.add(
HttpApiEndpoint.post("compact", "/api/session/:sessionID/compact", {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: HttpApiSchema.NoContent,
error: [SessionNotFoundError, ServiceUnavailableError],
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.session.compact",
summary: "Compact v2 session",
description: "Compact a v2 session conversation.",
}),
),
)
.add(
HttpApiEndpoint.post("wait", "/api/session/:sessionID/wait", {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: HttpApiSchema.NoContent,
error: [SessionNotFoundError, ServiceUnavailableError],
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.session.wait",
summary: "Wait for v2 session",
description: "Wait for a v2 session agent loop to become idle.",
}),
),
)
.add(
HttpApiEndpoint.get("context", "/api/session/:sessionID/context", {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: Schema.Array(SessionMessage.Message),
error: [SessionNotFoundError, UnknownError],
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.session.context",
summary: "Get v2 session context",
description: "Retrieve the active context messages for a v2 session (all messages after the last compaction).",
}),
),
)
.annotateMerge(
OpenApi.annotations({
title: "v2",
description: "Experimental v2 routes.",
}),
)
.middleware(V2Authorization)
@@ -1,47 +0,0 @@
import { SessionV2 } from "@opencode-ai/core/session"
import { Database } from "@opencode-ai/core/database/database"
import { EventV2 } from "@opencode-ai/core/event"
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
import { ProjectV2 } from "@opencode-ai/core/project"
import * as SessionExecutionLocal from "@opencode-ai/core/session/execution/local"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { SessionStore } from "@opencode-ai/core/session/store"
import { Layer } from "effect"
import { layer as v2LocationLayer } from "../groups/v2/location"
import { messageHandlers } from "./v2/message"
import { modelHandlers } from "./v2/model"
import { providerHandlers } from "./v2/provider"
import { sessionHandlers } from "./v2/session"
import { permissionHandlers, savedPermissionHandlers, sessionPermissionHandlers } from "./v2/permission"
import { fileSystemHandlers } from "./v2/fs"
import { questionHandlers, sessionQuestionHandlers } from "./v2/question"
const routedSessions = SessionV2.layer.pipe(
Layer.provide(SessionProjector.layer),
Layer.provide(SessionExecutionLocal.layer),
Layer.provide(LocationServiceMap.layer),
Layer.provide(SessionStore.layer),
Layer.provide(EventV2.layer),
Layer.provide(Database.defaultLayer),
Layer.provide(ProjectV2.defaultLayer),
Layer.orDie,
)
export const v2Handlers = Layer.mergeAll(
sessionHandlers,
messageHandlers,
modelHandlers,
providerHandlers,
permissionHandlers,
sessionPermissionHandlers,
savedPermissionHandlers,
fileSystemHandlers,
questionHandlers,
sessionQuestionHandlers,
).pipe(
Layer.provide(v2LocationLayer),
Layer.provide(LocationServiceMap.layer),
Layer.provide(PermissionSaved.layer),
Layer.provide(routedSessions),
)
@@ -1,12 +0,0 @@
import { FileSystem } from "@opencode-ai/core/filesystem"
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../../api"
export const fileSystemHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.fs", (handlers) =>
Effect.gen(function* () {
return handlers
.handle("read", (ctx) => FileSystem.Service.use((fs) => fs.read(ctx.query)))
.handle("list", (ctx) => FileSystem.Service.use((fs) => fs.list(ctx.query)))
}),
)
@@ -1,84 +0,0 @@
import { SessionMessage } from "@opencode-ai/core/session/message"
import { SessionV2 } from "@opencode-ai/core/session"
import { Effect, Schema } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../../api"
import { InvalidCursorError, SessionNotFoundError, UnknownError } from "../../errors"
const DefaultMessagesLimit = 50
const Cursor = Schema.Struct({
id: SessionMessage.ID,
order: Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")]),
direction: Schema.Union([Schema.Literal("previous"), Schema.Literal("next")]),
})
const decodeCursor = Schema.decodeUnknownSync(Cursor)
const cursor = {
encode(message: SessionMessage.Message, order: "asc" | "desc", direction: "previous" | "next") {
return Buffer.from(JSON.stringify({ id: message.id, order, direction })).toString("base64url")
},
decode(input: string) {
return decodeCursor(JSON.parse(Buffer.from(input, "base64url").toString("utf8")))
},
}
export const messageHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.message", (handlers) =>
Effect.gen(function* () {
const session = yield* SessionV2.Service
return handlers.handle(
"messages",
Effect.fn(function* (ctx) {
if (ctx.query.cursor && ctx.query.order !== undefined)
return yield* new InvalidCursorError({ message: "Cursor cannot be combined with order" })
const decoded = yield* Effect.try({
try: () => (ctx.query.cursor ? cursor.decode(ctx.query.cursor) : undefined),
catch: () => new InvalidCursorError({ message: "Invalid cursor" }),
})
const order = decoded?.order ?? ctx.query.order ?? "desc"
const messages = yield* session
.messages({
sessionID: ctx.params.sessionID,
limit: ctx.query.limit ?? DefaultMessagesLimit,
order,
cursor: decoded ? { id: decoded.id, direction: decoded.direction } : undefined,
})
.pipe(
Effect.catchTag("Session.NotFoundError", (error) =>
Effect.fail(
new SessionNotFoundError({
sessionID: error.sessionID,
message: `Session not found: ${error.sessionID}`,
}),
),
),
Effect.catchTag("Session.MessageDecodeError", (error) => {
const ref = `err_${crypto.randomUUID().slice(0, 8)}`
return Effect.logError("failed to decode v2 session message").pipe(
Effect.annotateLogs({ ref, sessionID: error.sessionID, messageID: error.messageID }),
Effect.andThen(
Effect.fail(
new UnknownError({
message: "Unexpected server error. Check server logs for details.",
ref,
}),
),
),
)
}),
)
const first = messages[0]
const last = messages.at(-1)
return {
items: messages,
cursor: {
previous: first ? cursor.encode(first, order, "previous") : undefined,
next: last ? cursor.encode(last, order, "next") : undefined,
},
}
}),
)
}),
)
@@ -1,26 +0,0 @@
import { Catalog } from "@opencode-ai/core/catalog"
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
import { ModelV2 } from "@opencode-ai/core/model"
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../../api"
import { ServiceUnavailableError } from "../../errors"
const catalogUnavailable = new ServiceUnavailableError({
message: "Model catalog is unavailable",
service: "catalog",
})
export const modelHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.model", (handlers) =>
Effect.gen(function* () {
return handlers.handle(
"models",
Effect.fn(function* () {
const catalog = yield* Catalog.Service
const pluginBoot = yield* PluginBoot.Service
yield* pluginBoot.wait().pipe(Effect.catchDefect(() => Effect.fail(catalogUnavailable)))
return (yield* catalog.model.available()).map(ModelV2.toPublic)
}),
)
}),
)
@@ -1,105 +0,0 @@
import { Database } from "@opencode-ai/core/database/database"
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { PermissionV2 } from "@opencode-ai/core/permission"
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionTable } from "@opencode-ai/core/session/sql"
import { eq } from "drizzle-orm"
import { Effect } from "effect"
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../../api"
import { PermissionNotFoundError, SessionNotFoundError } from "../../errors"
function missingRequest(id: PermissionV2.ID) {
return new PermissionNotFoundError({ requestID: id, message: `Permission request not found: ${id}` })
}
export const permissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.permission", (handlers) =>
Effect.gen(function* () {
return handlers.handle(
"permissionRequests",
Effect.fn(function* () {
return yield* (yield* PermissionV2.Service).list()
}),
)
}),
)
export const sessionPermissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session.permission", (handlers) =>
Effect.gen(function* () {
const { db } = yield* Database.Service
const locations = yield* LocationServiceMap
const withSessionPermission = Effect.fnUntraced(function* <A, E>(
sessionID: Parameters<PermissionV2.Interface["forSession"]>[0],
use: (permission: PermissionV2.Interface) => Effect.Effect<A, E>,
) {
const row = yield* db
.select({ directory: SessionTable.directory, workspaceID: SessionTable.workspace_id })
.from(SessionTable)
.where(eq(SessionTable.id, sessionID))
.get()
.pipe(Effect.orDie)
if (!row)
return yield* new SessionNotFoundError({
sessionID,
message: `Session not found: ${sessionID}`,
})
return yield* Effect.gen(function* () {
return yield* use(yield* PermissionV2.Service)
}).pipe(
Effect.scoped,
Effect.provide(
locations.get({ directory: AbsolutePath.make(row.directory), workspaceID: row.workspaceID ?? undefined }),
),
)
})
return handlers
.handle(
"sessionPermissionRequests",
Effect.fn(function* (ctx) {
return yield* withSessionPermission(ctx.params.sessionID, (permission) =>
permission.forSession(ctx.params.sessionID),
)
}),
)
.handle(
"permissionRequestReply",
Effect.fn(function* (ctx) {
yield* withSessionPermission(ctx.params.sessionID, (permission) =>
Effect.gen(function* () {
const request = yield* permission.get(ctx.params.requestID)
if (!request || request.sessionID !== ctx.params.sessionID)
return yield* missingRequest(ctx.params.requestID)
yield* permission
.reply({ requestID: ctx.params.requestID, reply: ctx.payload.reply, message: ctx.payload.message })
.pipe(Effect.catchTag("PermissionV2.NotFoundError", () => missingRequest(ctx.params.requestID)))
}),
)
return HttpApiSchema.NoContent.make()
}),
)
}),
)
export const savedPermissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.permission.saved", (handlers) =>
Effect.gen(function* () {
const saved = yield* PermissionSaved.Service
return handlers
.handle(
"savedPermissions",
Effect.fn(function* (ctx) {
return yield* saved.list({ projectID: ctx.query.projectID })
}),
)
.handle(
"removeSavedPermission",
Effect.fn(function* (ctx) {
yield* saved.remove(ctx.params.id)
return HttpApiSchema.NoContent.make()
}),
)
}),
)
@@ -1,46 +0,0 @@
import { Catalog } from "@opencode-ai/core/catalog"
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../../api"
import { ProviderNotFoundError, ServiceUnavailableError } from "../../errors"
const catalogUnavailable = new ServiceUnavailableError({
message: "Provider catalog is unavailable",
service: "catalog",
})
export const providerHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.provider", (handlers) =>
Effect.gen(function* () {
return handlers
.handle(
"providers",
Effect.fn(function* () {
const catalog = yield* Catalog.Service
const pluginBoot = yield* PluginBoot.Service
yield* pluginBoot.wait().pipe(Effect.catchDefect(() => Effect.fail(catalogUnavailable)))
return (yield* catalog.provider.available()).map(ProviderV2.toPublic)
}),
)
.handle(
"provider",
Effect.fn(function* (ctx) {
const catalog = yield* Catalog.Service
const pluginBoot = yield* PluginBoot.Service
yield* pluginBoot.wait().pipe(Effect.catchDefect(() => Effect.fail(catalogUnavailable)))
return yield* catalog.provider.get(ctx.params.providerID).pipe(
Effect.map(ProviderV2.toPublic),
Effect.catchTag("CatalogV2.ProviderNotFound", (error) =>
Effect.fail(
new ProviderNotFoundError({
providerID: error.providerID,
message: `Provider not found: ${error.providerID}`,
}),
),
),
)
}),
)
}),
)
@@ -1,96 +0,0 @@
import { Database } from "@opencode-ai/core/database/database"
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { QuestionV2 } from "@opencode-ai/core/question"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionTable } from "@opencode-ai/core/session/sql"
import { eq } from "drizzle-orm"
import { Effect } from "effect"
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../../api"
import { QuestionNotFoundError, SessionNotFoundError } from "../../errors"
function missingRequest(id: QuestionV2.ID) {
return new QuestionNotFoundError({ requestID: id, message: `Question request not found: ${id}` })
}
export const questionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.question", (handlers) =>
Effect.gen(function* () {
return handlers.handle(
"questionRequests",
Effect.fn(function* () {
return yield* (yield* QuestionV2.Service).list()
}),
)
}),
)
export const sessionQuestionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session.question", (handlers) =>
Effect.gen(function* () {
const { db } = yield* Database.Service
const locations = yield* LocationServiceMap
const withSessionQuestion = Effect.fnUntraced(function* <A, E>(
sessionID: QuestionV2.Request["sessionID"],
use: (question: QuestionV2.Interface) => Effect.Effect<A, E>,
) {
const row = yield* db
.select({ directory: SessionTable.directory, workspaceID: SessionTable.workspace_id })
.from(SessionTable)
.where(eq(SessionTable.id, sessionID))
.get()
.pipe(Effect.orDie)
if (!row)
return yield* new SessionNotFoundError({
sessionID,
message: `Session not found: ${sessionID}`,
})
return yield* Effect.gen(function* () {
return yield* use(yield* QuestionV2.Service)
}).pipe(
Effect.scoped,
Effect.provide(
locations.get({ directory: AbsolutePath.make(row.directory), workspaceID: row.workspaceID ?? undefined }),
),
)
})
const withOwnedQuestion = Effect.fnUntraced(function* <A, E>(
sessionID: QuestionV2.Request["sessionID"],
requestID: QuestionV2.ID,
use: (question: QuestionV2.Interface) => Effect.Effect<A, E>,
) {
return yield* withSessionQuestion(sessionID, (question) =>
Effect.gen(function* () {
const request = (yield* question.list()).find((request) => request.id === requestID)
if (!request || request.sessionID !== sessionID) return yield* missingRequest(requestID)
return yield* use(question)
}),
)
})
return handlers
.handle(
"questionRequestReply",
Effect.fn(function* (ctx) {
yield* withOwnedQuestion(ctx.params.sessionID, ctx.params.requestID, (question) =>
question
.reply({ requestID: ctx.params.requestID, answers: ctx.payload.answers })
.pipe(Effect.catchTag("QuestionV2.NotFoundError", () => missingRequest(ctx.params.requestID))),
)
return HttpApiSchema.NoContent.make()
}),
)
.handle(
"questionRequestReject",
Effect.fn(function* (ctx) {
yield* withOwnedQuestion(ctx.params.sessionID, ctx.params.requestID, (question) =>
question
.reject(ctx.params.requestID)
.pipe(Effect.catchTag("QuestionV2.NotFoundError", () => missingRequest(ctx.params.requestID))),
)
return HttpApiSchema.NoContent.make()
}),
)
}),
)
@@ -1,173 +0,0 @@
import { SessionV2 } from "@opencode-ai/core/session"
import { DateTime, Effect } from "effect"
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../../api"
import { SessionsCursor } from "../../groups/v2/session"
import {
ConflictError,
InvalidCursorError,
ServiceUnavailableError,
SessionNotFoundError,
UnknownError,
} from "../../errors"
const DefaultSessionsLimit = 50
export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session", (handlers) =>
Effect.gen(function* () {
const session = yield* SessionV2.Service
return handlers
.handle(
"sessions",
Effect.fn(function* (ctx) {
const query =
ctx.query.cursor !== undefined
? yield* SessionsCursor.parse(ctx.query.cursor).pipe(
Effect.mapError(() => new InvalidCursorError({ message: "Invalid cursor" })),
)
: ctx.query
const sessions = yield* session.list({
...query,
workspaceID: query.workspace,
limit: ctx.query.limit ?? DefaultSessionsLimit,
})
const first = sessions[0]
const last = sessions.at(-1)
return {
items: sessions,
cursor: {
previous: first
? SessionsCursor.make({
...query,
anchor: {
id: first.id,
time: DateTime.toEpochMillis(first.time.created),
direction: "previous",
},
})
: undefined,
next: last
? SessionsCursor.make({
...query,
anchor: {
id: last.id,
time: DateTime.toEpochMillis(last.time.created),
direction: "next",
},
})
: undefined,
},
}
}),
)
.handle(
"prompt",
Effect.fn(function* (ctx) {
return yield* session
.prompt({
sessionID: ctx.params.sessionID,
id: ctx.payload.id,
prompt: ctx.payload.prompt,
delivery: ctx.payload.delivery,
resume: ctx.payload.resume,
})
.pipe(
Effect.catchTag("Session.NotFoundError", (error) =>
Effect.fail(
new SessionNotFoundError({
sessionID: error.sessionID,
message: `Session not found: ${error.sessionID}`,
}),
),
),
Effect.catchTag("Session.PromptConflictError", (error) =>
Effect.fail(
new ConflictError({
message: `Prompt message ID conflicts with an existing durable record: ${error.messageID}`,
resource: error.messageID,
}),
),
),
)
}),
)
.handle(
"compact",
Effect.fn(function* (ctx) {
yield* session.compact({ sessionID: ctx.params.sessionID }).pipe(
Effect.catchTag("Session.NotFoundError", (error) =>
Effect.fail(
new SessionNotFoundError({
sessionID: error.sessionID,
message: `Session not found: ${error.sessionID}`,
}),
),
),
Effect.catchTag("Session.OperationUnavailableError", (error) =>
Effect.fail(
new ServiceUnavailableError({
message: `V2 session ${error.operation} is not available yet`,
service: `v2.session.${error.operation}`,
}),
),
),
)
return HttpApiSchema.NoContent.make()
}),
)
.handle(
"wait",
Effect.fn(function* (ctx) {
yield* session.wait(ctx.params.sessionID).pipe(
Effect.catchTag("Session.NotFoundError", (error) =>
Effect.fail(
new SessionNotFoundError({
sessionID: error.sessionID,
message: `Session not found: ${error.sessionID}`,
}),
),
),
Effect.catchTag("Session.OperationUnavailableError", (error) =>
Effect.fail(
new ServiceUnavailableError({
message: `V2 session ${error.operation} is not available yet`,
service: `v2.session.${error.operation}`,
}),
),
),
)
return HttpApiSchema.NoContent.make()
}),
)
.handle(
"context",
Effect.fn(function* (ctx) {
return yield* session.context(ctx.params.sessionID).pipe(
Effect.catchTag("Session.NotFoundError", (error) =>
Effect.fail(
new SessionNotFoundError({
sessionID: error.sessionID,
message: `Session not found: ${error.sessionID}`,
}),
),
),
Effect.catchTag("Session.MessageDecodeError", (error) => {
const ref = `err_${crypto.randomUUID().slice(0, 8)}`
return Effect.logError("failed to decode v2 session message").pipe(
Effect.annotateLogs({ ref, sessionID: error.sessionID, messageID: error.messageID }),
Effect.andThen(
Effect.fail(
new UnknownError({
message: "Unexpected server error. Check server logs for details.",
ref,
}),
),
),
)
}),
)
}),
)
}),
)
@@ -4,7 +4,7 @@ import { HttpEffect, HttpRouter, HttpServerRequest, HttpServerResponse } from "e
import { HttpApiError, HttpApiMiddleware } from "effect/unstable/httpapi"
import { hasPtyConnectTicketURL } from "@/server/shared/pty-ticket"
import { isPublicUIPath } from "@/server/shared/public-ui"
import { UnauthorizedError } from "../errors"
export { V2Authorization, v2AuthorizationLayer } from "@opencode-ai/server/middleware/authorization"
const AUTH_TOKEN_QUERY = "auth_token"
const UNAUTHORIZED = 401
@@ -20,13 +20,6 @@ export class Authorization extends HttpApiMiddleware.Service<Authorization>()(
},
) {}
export class V2Authorization extends HttpApiMiddleware.Service<V2Authorization>()(
"@opencode/ExperimentalHttpApiV2Authorization",
{
error: UnauthorizedError,
},
) {}
export class PtyConnectAuthorization extends HttpApiMiddleware.Service<PtyConnectAuthorization>()(
"@opencode/ExperimentalHttpApiPtyConnectAuthorization",
{
@@ -152,27 +145,3 @@ export const ptyConnectAuthorizationLayer = Layer.effect(
)
}),
)
export const v2AuthorizationLayer = Layer.effect(
V2Authorization,
Effect.gen(function* () {
const config = yield* ServerAuth.Config
if (!ServerAuth.required(config)) return V2Authorization.of((effect) => effect)
return V2Authorization.of((effect) =>
Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest
return yield* credentialFromRequest(request).pipe(
Effect.flatMap((credential) =>
Effect.gen(function* () {
if (ServerAuth.authorized(credential, config)) return yield* effect
yield* HttpEffect.appendPreResponseHandler((_request, response) =>
Effect.succeed(HttpServerResponse.setHeader(response, "www-authenticate", WWW_AUTHENTICATE)),
)
return yield* new UnauthorizedError({ message: "Authentication required" })
}),
),
)
}),
)
}),
)
@@ -44,6 +44,7 @@ import { Todo } from "@/session/todo"
import { SessionShare } from "@/share/session"
import { ShareNext } from "@/share/share-next"
import { EventV2Bridge } from "@/event-v2-bridge"
import { EventV2 } from "@opencode-ai/core/event"
import { Database } from "@opencode-ai/core/database/database"
import { Skill } from "@/skill"
import { Snapshot } from "@/snapshot"
@@ -56,6 +57,7 @@ import { CorsConfig, isAllowedCorsOrigin, type CorsOptions } from "@/server/cors
import { serveUIEffect } from "@/server/shared/ui"
import { ServerAuth } from "@/server/auth"
import { InstanceHttpApi, RootHttpApi } from "./api"
import { V2Api } from "@opencode-ai/server/api"
import { PublicApi } from "./public"
import {
authorizationLayer,
@@ -82,7 +84,8 @@ import { questionHandlers } from "./handlers/question"
import { sessionHandlers } from "./handlers/session"
import { syncHandlers } from "./handlers/sync"
import { tuiHandlers } from "./handlers/tui"
import { v2Handlers } from "./handlers/v2"
import { v2Handlers } from "@opencode-ai/server/handlers"
import { schemaErrorLayer as v2SchemaErrorLayer } from "@opencode-ai/server/middleware/schema-error"
import { workspaceHandlers } from "./handlers/workspace"
import { instanceContextLayer } from "./middleware/instance-context"
import { workspaceRoutingLayer } from "./middleware/workspace-routing"
@@ -144,14 +147,17 @@ const instanceApiRoutes = HttpApiBuilder.layer(InstanceHttpApi).pipe(
providerHandlers,
sessionHandlers,
syncHandlers,
v2Handlers,
tuiHandlers,
workspaceHandlers,
]),
)
const instanceRoutes = instanceApiRoutes.pipe(
Layer.provide([httpApiAuthLayer, v2HttpApiAuthLayer, workspaceRoutingLive, instanceContextLayer, schemaErrorLayer]),
Layer.provide([httpApiAuthLayer, workspaceRoutingLive, instanceContextLayer, schemaErrorLayer]),
)
const v2Routes = HttpApiBuilder.layer(V2Api).pipe(
Layer.provide(v2Handlers),
Layer.provide([v2HttpApiAuthLayer, v2SchemaErrorLayer]),
)
// `OpenApi.fromApi` is non-trivial; defer until /doc is actually hit so
@@ -186,7 +192,7 @@ type RouteRequirements =
export function createRoutes(
corsOptions?: CorsOptions,
): Layer.Layer<never, EffectConfig.ConfigError, RouteRequirements> {
return Layer.mergeAll(rootApiRoutes, eventApiRoutes, ptyConnectApiRoutes, instanceRoutes, docRoute, uiRoute).pipe(
return Layer.mergeAll(rootApiRoutes, eventApiRoutes, ptyConnectApiRoutes, instanceRoutes, v2Routes, docRoute, uiRoute).pipe(
Layer.provide([
errorLayer,
compressionLayer,
@@ -226,6 +232,7 @@ export function createRoutes(
ShareNext.defaultLayer,
Snapshot.defaultLayer,
EventV2Bridge.defaultLayer,
EventV2.defaultLayer,
Skill.defaultLayer,
Todo.defaultLayer,
ToolRegistry.defaultLayer,