From 1508196c0f4f4892325accddb5affeadbc4e8574 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:50:22 -0400 Subject: [PATCH] feat: bridge question routes from Hono to Effect HttpApi (#22718) --- packages/opencode/src/flag/flag.ts | 1 + .../src/server/instance/httpapi/question.ts | 25 ++++++++++--- .../src/server/instance/httpapi/server.ts | 36 +++++++++---------- .../opencode/src/server/instance/index.ts | 14 ++++++-- 4 files changed, 49 insertions(+), 27 deletions(-) diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index a63f8d1c66..21923f982f 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -84,6 +84,7 @@ export namespace Flag { export const OPENCODE_STRICT_CONFIG_DEPS = truthy("OPENCODE_STRICT_CONFIG_DEPS") export const OPENCODE_WORKSPACE_ID = process.env["OPENCODE_WORKSPACE_ID"] + export const OPENCODE_EXPERIMENTAL_HTTPAPI = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_HTTPAPI") export const OPENCODE_EXPERIMENTAL_WORKSPACES = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_WORKSPACES") function number(key: string) { diff --git a/packages/opencode/src/server/instance/httpapi/question.ts b/packages/opencode/src/server/instance/httpapi/question.ts index 686c6abb17..51966d13b9 100644 --- a/packages/opencode/src/server/instance/httpapi/question.ts +++ b/packages/opencode/src/server/instance/httpapi/question.ts @@ -3,7 +3,7 @@ import { QuestionID } from "@/question/schema" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" -const root = "/experimental/httpapi/question" +const root = "/question" export const QuestionApi = HttpApi.make("question") .add( @@ -29,19 +29,29 @@ export const QuestionApi = HttpApi.make("question") description: "Provide answers to a question request from the AI assistant.", }), ), + HttpApiEndpoint.post("reject", `${root}/:requestID/reject`, { + params: { requestID: QuestionID }, + success: Schema.Boolean, + }).annotateMerge( + OpenApi.annotations({ + identifier: "question.reject", + summary: "Reject question request", + description: "Reject a question request from the AI assistant.", + }), + ), ) .annotateMerge( OpenApi.annotations({ title: "question", - description: "Experimental HttpApi question routes.", + description: "Question routes.", }), ), ) .annotateMerge( OpenApi.annotations({ - title: "opencode experimental HttpApi", + title: "opencode HttpApi", version: "0.0.1", - description: "Experimental HttpApi surface for selected instance routes.", + description: "Effect HttpApi surface for instance routes.", }), ) @@ -64,8 +74,13 @@ export const QuestionLive = Layer.unwrap( return true }) + const reject = Effect.fn("QuestionHttpApi.reject")(function* (ctx: { params: { requestID: QuestionID } }) { + yield* svc.reject(ctx.params.requestID) + return true + }) + return HttpApiBuilder.group(QuestionApi, "question", (handlers) => - handlers.handle("list", list).handle("reply", reply), + handlers.handle("list", list).handle("reply", reply).handle("reject", reject), ) }), ).pipe(Layer.provide(Question.defaultLayer)) diff --git a/packages/opencode/src/server/instance/httpapi/server.ts b/packages/opencode/src/server/instance/httpapi/server.ts index 9894343c56..2ca692efbe 100644 --- a/packages/opencode/src/server/instance/httpapi/server.ts +++ b/packages/opencode/src/server/instance/httpapi/server.ts @@ -1,17 +1,15 @@ -import { NodeHttpServer } from "@effect/platform-node" import { Effect, Layer, Redacted, Schema } from "effect" import { HttpApiBuilder, HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi" -import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" -import { createServer } from "node:http" +import { HttpRouter, HttpServer, HttpServerRequest } from "effect/unstable/http" import { AppRuntime } from "@/effect/app-runtime" import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref" +import { Observability } from "@/effect/observability" +import { memoMap } from "@/effect/run-service" import { Flag } from "@/flag/flag" import { InstanceBootstrap } from "@/project/bootstrap" import { Instance } from "@/project/instance" +import { lazy } from "@/util/lazy" import { Filesystem } from "@/util/filesystem" -import { Permission } from "@/permission" -import { ProviderAuth } from "@/provider/auth" -import { Question } from "@/question" import { PermissionApi, PermissionLive } from "./permission" import { ProviderApi, ProviderLive } from "./provider" import { QuestionApi, QuestionLive } from "./question" @@ -113,26 +111,24 @@ export namespace ExperimentalHttpApiServer { const ProviderSecured = ProviderApi.middleware(Authorization) export const routes = Layer.mergeAll( - HttpApiBuilder.layer(QuestionSecured, { openapiPath: "/experimental/httpapi/question/doc" }).pipe( - Layer.provide(QuestionLive), - ), + HttpApiBuilder.layer(QuestionSecured).pipe(Layer.provide(QuestionLive)), HttpApiBuilder.layer(PermissionSecured, { openapiPath: "/experimental/httpapi/permission/doc" }).pipe( Layer.provide(PermissionLive), ), HttpApiBuilder.layer(ProviderSecured, { openapiPath: "/experimental/httpapi/provider/doc" }).pipe( Layer.provide(ProviderLive), ), - ).pipe(Layer.provide(auth), Layer.provide(normalize), Layer.provide(instance)) + ).pipe( + Layer.provide(auth), + Layer.provide(normalize), + Layer.provide(instance), + Layer.provide(HttpServer.layerServices), + Layer.provideMerge(Observability.layer), + ) - export const layer = (opts: { hostname: string; port: number }) => - HttpRouter.serve(routes, { disableListenLog: true, disableLogger: true }).pipe( - Layer.provideMerge(NodeHttpServer.layer(createServer, { port: opts.port, host: opts.hostname })), - ) - - export const layerTest = HttpRouter.serve(routes, { disableListenLog: true, disableLogger: true }).pipe( - Layer.provideMerge(NodeHttpServer.layerTest), - Layer.provideMerge(Question.defaultLayer), - Layer.provideMerge(Permission.defaultLayer), - Layer.provideMerge(ProviderAuth.defaultLayer), + export const webHandler = lazy(() => + HttpRouter.toWebHandler(routes, { + memoMap, + }), ) } diff --git a/packages/opencode/src/server/instance/index.ts b/packages/opencode/src/server/instance/index.ts index 4a03b7b29c..950b9a8588 100644 --- a/packages/opencode/src/server/instance/index.ts +++ b/packages/opencode/src/server/instance/index.ts @@ -14,6 +14,8 @@ import { LSP } from "../../lsp" import { Command } from "../../command" import { QuestionRoutes } from "./question" import { PermissionRoutes } from "./permission" +import { Flag } from "@/flag/flag" +import { ExperimentalHttpApiServer } from "./httpapi/server" import { ProjectRoutes } from "./project" import { SessionRoutes } from "./session" import { PtyRoutes } from "./pty" @@ -27,8 +29,8 @@ import { SyncRoutes } from "./sync" import { WorkspaceRouterMiddleware } from "./middleware" import { AppRuntime } from "@/effect/app-runtime" -export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => - new Hono() +export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { + const app = new Hono() .use(WorkspaceRouterMiddleware(upgrade)) .route("/project", ProjectRoutes()) .route("/pty", PtyRoutes(upgrade)) @@ -36,6 +38,13 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => .route("/experimental", ExperimentalRoutes()) .route("/session", SessionRoutes()) .route("/permission", PermissionRoutes()) + + if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) { + const handler = ExperimentalHttpApiServer.webHandler().handler + app.all("/question", (c) => handler(c.req.raw)).all("/question/*", (c) => handler(c.req.raw)) + } + + return app .route("/question", QuestionRoutes()) .route("/provider", ProviderRoutes()) .route("/sync", SyncRoutes()) @@ -283,3 +292,4 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => return c.json(await AppRuntime.runPromise(Format.Service.use((svc) => svc.status()))) }, ) +}