feat(core): global forms support (#35106)
This commit is contained in:
parent
d9bf30fc22
commit
c167bde6a0
12 changed files with 209 additions and 30 deletions
|
|
@ -1,8 +1,12 @@
|
|||
import { makeDefaultApi } from "@opencode-ai/protocol/api"
|
||||
import { LocationMiddleware } from "./location"
|
||||
import { FormLocationMiddleware } from "./middleware/form-location"
|
||||
import { SessionLocationMiddleware } from "./middleware/session-location"
|
||||
|
||||
export const Api = makeDefaultApi({
|
||||
locationMiddleware: LocationMiddleware,
|
||||
// FormLocationMiddleware contains the temporary `sessionID === "global"` MCP elicitation hack.
|
||||
// Do not use that sentinel with general session APIs.
|
||||
formLocationMiddleware: FormLocationMiddleware,
|
||||
sessionLocationMiddleware: SessionLocationMiddleware,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export function response<A, E, R>(data: Effect.Effect<A, E, R>) {
|
|||
})
|
||||
}
|
||||
|
||||
function ref(request: HttpServerRequest.HttpServerRequest): Location.Ref {
|
||||
export function requestRef(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"]
|
||||
const directory =
|
||||
|
|
@ -53,7 +53,7 @@ export const layer = Layer.effect(
|
|||
return LocationMiddleware.of((effect) =>
|
||||
Effect.gen(function* () {
|
||||
const request = yield* HttpServerRequest.HttpServerRequest
|
||||
return yield* effect.pipe(Effect.provide(locations.get(ref(request))))
|
||||
return yield* effect.pipe(Effect.provide(locations.get(requestRef(request))))
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
|
|
|||
76
packages/server/src/middleware/form-location.ts
Normal file
76
packages/server/src/middleware/form-location.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-services"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import { InvalidRequestError, SessionNotFoundError } from "@opencode-ai/protocol/errors"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
import { HttpRouter, HttpServerRequest } from "effect/unstable/http"
|
||||
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
||||
import { requestRef, type LocationServices } from "../location"
|
||||
|
||||
export class FormLocationMiddleware extends HttpApiMiddleware.Service<
|
||||
FormLocationMiddleware,
|
||||
{ provides: LocationServices }
|
||||
>()("@opencode/HttpApiFormLocation", {
|
||||
error: [InvalidRequestError, SessionNotFoundError],
|
||||
}) {}
|
||||
|
||||
const decodeSessionID = Schema.decodeUnknownEffect(SessionV2.ID)
|
||||
|
||||
export const formLocationLayer = Layer.effect(
|
||||
FormLocationMiddleware,
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
|
||||
return FormLocationMiddleware.of((effect) =>
|
||||
Effect.gen(function* () {
|
||||
const route = yield* HttpRouter.RouteContext
|
||||
if (route.params.sessionID === "global") {
|
||||
// Temporary MCP elicitation escape hatch. This is still Location-scoped; it only bypasses
|
||||
// the session row lookup because some MCP elicitations cannot currently be attributed to
|
||||
// a real session. Keep this undocumented and remove once elicitations carry session ownership.
|
||||
const request = yield* HttpServerRequest.HttpServerRequest
|
||||
return yield* effect.pipe(Effect.provide(locations.get(requestRef(request))))
|
||||
}
|
||||
|
||||
const sessionID = yield* decodeSessionID(route.params.sessionID).pipe(
|
||||
Effect.mapError(
|
||||
() =>
|
||||
new InvalidRequestError({
|
||||
message: "Invalid session ID",
|
||||
field: "sessionID",
|
||||
}),
|
||||
),
|
||||
)
|
||||
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.pipe(
|
||||
Effect.provide(
|
||||
locations.get(
|
||||
Location.Ref.make({
|
||||
directory: AbsolutePath.make(row.directory),
|
||||
workspaceID: row.workspaceID ? WorkspaceV2.ID.make(row.workspaceID) : undefined,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
|
@ -24,6 +24,7 @@ import { authorizationLayer } from "./middleware/authorization"
|
|||
import { schemaErrorLayer } from "./middleware/schema-error"
|
||||
import { PtyEnvironment } from "./pty-environment"
|
||||
import { layer as locationLayer } from "./location"
|
||||
import { formLocationLayer } from "./middleware/form-location"
|
||||
import { sessionLocationLayer } from "./middleware/session-location"
|
||||
|
||||
const applicationServices = LayerNode.group([
|
||||
|
|
@ -79,6 +80,7 @@ function makeRoutes<AuthError, AuthServices>(
|
|||
|
||||
return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(
|
||||
Layer.provide(handlers),
|
||||
Layer.provide(formLocationLayer),
|
||||
Layer.provide(sessionLocationLayer),
|
||||
Layer.provide(locationLayer),
|
||||
Layer.provide(authorizationLayer),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue