From 816ba270a50bae52c18a92d29425e4fcb26968c1 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Wed, 15 Apr 2026 05:10:15 +0530 Subject: [PATCH] feat(server): mount question web handler --- .../src/server/instance/httpapi/question.ts | 29 +++--------- packages/server/src/api/index.ts | 1 + packages/server/src/api/question.ts | 46 ++++++++++++++++++- packages/server/src/definition/question.ts | 6 +-- packages/server/src/index.ts | 3 +- 5 files changed, 57 insertions(+), 28 deletions(-) diff --git a/packages/opencode/src/server/instance/httpapi/question.ts b/packages/opencode/src/server/instance/httpapi/question.ts index ef0f41734b..e1d0228fe0 100644 --- a/packages/opencode/src/server/instance/httpapi/question.ts +++ b/packages/opencode/src/server/instance/httpapi/question.ts @@ -2,15 +2,10 @@ import { AppLayer } from "@/effect/app-runtime" import { memoMap } from "@/effect/run-service" import { Question } from "@/question" import { QuestionID } from "@/question/schema" -import { lazy } from "@/util/lazy" -import { makeQuestionHandler, questionApi } from "@opencode-ai/server" +import { makeQuestionHandler, makeQuestionWebHandler } from "@opencode-ai/server" import { Effect, Layer } from "effect" -import { HttpRouter, HttpServer } from "effect/unstable/http" -import { HttpApiBuilder } from "effect/unstable/httpapi" import type { Handler } from "hono" -const root = "/experimental/httpapi/question" - const QuestionLive = makeQuestionHandler({ list: Effect.fn("QuestionHttpApi.host.list")(function* () { const svc = yield* Question.Service @@ -25,20 +20,10 @@ const QuestionLive = makeQuestionHandler({ }), }).pipe(Layer.provide(Question.defaultLayer)) -const web = lazy(() => - HttpRouter.toWebHandler( - Layer.mergeAll( - AppLayer, - HttpApiBuilder.layer(questionApi, { openapiPath: `${root}/doc` }).pipe( - Layer.provide(QuestionLive), - Layer.provide(HttpServer.layerServices), - ), - ), - { - disableLogger: true, - memoMap, - }, - ), -) +const web = makeQuestionWebHandler({ + app: AppLayer, + live: QuestionLive, + memoMap, +}) -export const QuestionHttpApiHandler: Handler = (c, _next) => web().handler(c.req.raw) +export const QuestionHttpApiHandler: Handler = (c, _next) => web(c.req.raw) diff --git a/packages/server/src/api/index.ts b/packages/server/src/api/index.ts index 375e3584b4..0937b5df11 100644 --- a/packages/server/src/api/index.ts +++ b/packages/server/src/api/index.ts @@ -1,2 +1,3 @@ export { makeQuestionHandler } from "./question.js" +export { makeQuestionWebHandler } from "./question.js" export type { QuestionOps } from "./question.js" diff --git a/packages/server/src/api/question.ts b/packages/server/src/api/question.ts index f72c37aa19..b649980e73 100644 --- a/packages/server/src/api/question.ts +++ b/packages/server/src/api/question.ts @@ -1,6 +1,7 @@ -import { Effect, Schema } from "effect" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter, HttpServer } from "effect/unstable/http" import { HttpApiBuilder } from "effect/unstable/httpapi" -import { QuestionReply, QuestionRequest, questionApi } from "../definition/question.js" +import { QuestionReply, QuestionRequest, questionApi, questionRoot } from "../definition/question.js" export interface QuestionOps { readonly list: () => Effect.Effect, never, R> @@ -35,3 +36,44 @@ export const makeQuestionHandler = (ops: QuestionOps) => return handlers.handle("list", list).handle("reply", reply) }), ) + +export const makeQuestionWebHandler = (opts: { + readonly app: Layer.Layer + readonly live: Layer.Layer + readonly memoMap?: Layer.MemoMap +}) => { + const app = Layer.mergeAll( + opts.app, + HttpApiBuilder.layer(questionApi, { openapiPath: `${questionRoot}/doc` }).pipe( + Layer.provide(opts.live), + Layer.provide(HttpServer.layerServices), + ), + ) + + const init = () => + HttpRouter.toWebHandler( + app as Layer.Layer< + A, + E | F, + | HttpRouter.HttpRouter + | HttpRouter.Request<"Requires", unknown> + | HttpRouter.Request<"GlobalRequires", unknown> + | HttpRouter.Request<"Error", unknown> + | HttpRouter.Request<"GlobalError", unknown> + >, + { + disableLogger: true, + memoMap: opts.memoMap, + }, + ) as { + readonly handler: (request: Request) => Promise + readonly dispose: () => Promise + } + + let web: ReturnType | undefined + + return (request: Request) => { + web ??= init() + return web.handler(request) + } +} diff --git a/packages/server/src/definition/question.ts b/packages/server/src/definition/question.ts index 0d161e013d..6d33aeb6be 100644 --- a/packages/server/src/definition/question.ts +++ b/packages/server/src/definition/question.ts @@ -1,7 +1,7 @@ import { Schema } from "effect" import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" -const root = "/experimental/httpapi/question" +export const questionRoot = "/experimental/httpapi/question" // Temporary transport-local schemas until canonical question schemas move into packages/core. export const QuestionID = Schema.String.annotate({ identifier: "QuestionID" }) @@ -64,7 +64,7 @@ export class QuestionReply extends Schema.Class("QuestionReply")( export const questionApi = HttpApi.make("question").add( HttpApiGroup.make("question") .add( - HttpApiEndpoint.get("list", root, { + HttpApiEndpoint.get("list", questionRoot, { success: Schema.Array(QuestionRequest), }).annotateMerge( OpenApi.annotations({ @@ -73,7 +73,7 @@ export const questionApi = HttpApi.make("question").add( description: "Get all pending question requests across all sessions.", }), ), - HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, { + HttpApiEndpoint.post("reply", `${questionRoot}/:requestID/reply`, { params: { requestID: QuestionID }, payload: QuestionReply, success: Schema.Boolean, diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 67b82a0be5..9629bf7e71 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,6 +1,7 @@ export { openapi } from "./openapi.js" export { makeQuestionHandler } from "./api/question.js" +export { makeQuestionWebHandler } from "./api/question.js" export { api } from "./definition/api.js" -export { questionApi, QuestionReply, QuestionRequest } from "./definition/question.js" +export { questionApi, questionRoot, QuestionReply, QuestionRequest } from "./definition/question.js" export type { OpenApiSpec, ServerApi } from "./types.js" export type { QuestionOps } from "./api/question.js"