diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts index 4ebc1a6078..19ab4fbb1b 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts @@ -1,6 +1,6 @@ import { Context, Effect, Layer } from "effect" import { HttpApiBuilder } from "effect/unstable/httpapi" -import { FetchHttpClient, HttpMiddleware, HttpRouter, HttpServer } from "effect/unstable/http" +import { FetchHttpClient, HttpClient, HttpMiddleware, HttpRouter, HttpServer } from "effect/unstable/http" import * as Socket from "effect/unstable/socket/Socket" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Account } from "@/account/account" @@ -121,9 +121,16 @@ const instanceRoutes = Layer.mergeAll(rawInstanceRoutes, instanceApiRoutes).pipe ]), ) -const uiRoute = HttpRouter.add("*", "/*", (request) => - serveUIEffect(request).pipe(Effect.provide(AppFileSystem.defaultLayer), Effect.provide(FetchHttpClient.layer)), -).pipe(Layer.provide(authorizationRouterMiddleware.layer.pipe(Layer.provide(ServerAuthConfig.defaultLayer)))) +const uiRoute = Layer.effectDiscard( + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const client = yield* HttpClient.HttpClient + const router = yield* HttpRouter.HttpRouter + yield* router.add("*", "/*", (request) => serveUIEffect(request, { fs, client })) + }), +).pipe( + Layer.provide(authorizationRouterMiddleware.layer.pipe(Layer.provide(ServerAuthConfig.defaultLayer))), +) export const routes = Layer.mergeAll(rootApiRoutes, instanceRoutes, uiRoute).pipe( Layer.provide([ @@ -162,6 +169,8 @@ export const routes = Layer.mergeAll(rootApiRoutes, instanceRoutes, uiRoute).pip Workspace.defaultLayer, Worktree.defaultLayer, Bus.layer, + AppFileSystem.defaultLayer, + FetchHttpClient.layer, HttpServer.layerServices, ]), Layer.provideMerge(Observability.layer), diff --git a/packages/opencode/src/server/routes/ui.ts b/packages/opencode/src/server/routes/ui.ts index a8c23460b3..403d85d66c 100644 --- a/packages/opencode/src/server/routes/ui.ts +++ b/packages/opencode/src/server/routes/ui.ts @@ -79,7 +79,10 @@ export async function serveUI(request: Request) { return response } -export function serveUIEffect(request: HttpServerRequest.HttpServerRequest) { +export function serveUIEffect( + request: HttpServerRequest.HttpServerRequest, + services: { fs: AppFileSystem.Interface; client: HttpClient.HttpClient }, +) { return Effect.gen(function* () { const embeddedWebUI = yield* Effect.promise(() => embeddedUI()) const path = new URL(request.url, "http://localhost").pathname @@ -88,18 +91,17 @@ export function serveUIEffect(request: HttpServerRequest.HttpServerRequest) { const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null if (!match) return HttpServerResponse.jsonUnsafe({ error: "Not Found" }, { status: 404 }) - const fs = yield* AppFileSystem.Service - if (yield* fs.existsSafe(match)) { + if (yield* services.fs.existsSafe(match)) { const mime = getMimeType(match) ?? "text/plain" const headers = new Headers({ "content-type": mime }) if (mime.startsWith("text/html")) headers.set("content-security-policy", DEFAULT_CSP) - return HttpServerResponse.raw(yield* fs.readFile(match), { headers }) + return HttpServerResponse.raw(yield* services.fs.readFile(match), { headers }) } return HttpServerResponse.jsonUnsafe({ error: "Not Found" }, { status: 404 }) } - const response = yield* HttpClient.execute( + const response = yield* services.client.execute( HttpClientRequest.make(request.method)(upstreamURL(path), { headers: ProxyUtil.headers(request.headers, { host: UI_UPSTREAM.host }), body: requestBody(request), diff --git a/packages/opencode/test/server/httpapi-ui.test.ts b/packages/opencode/test/server/httpapi-ui.test.ts index d02564bda3..666d2f8f36 100644 --- a/packages/opencode/test/server/httpapi-ui.test.ts +++ b/packages/opencode/test/server/httpapi-ui.test.ts @@ -74,22 +74,26 @@ function app(input?: { password?: string; username?: string }) { function uiApp(input?: { password?: string; username?: string; client?: Layer.Layer }) { const handler = HttpRouter.toWebHandler( - HttpRouter.add("*", "/*", (request) => - serveUIEffect(request).pipe( - Effect.provide(AppFileSystem.defaultLayer), - Effect.provide(input?.client ?? httpClient(new Response("ui"))), - ), + Layer.effectDiscard( + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const client = yield* HttpClient.HttpClient + const router = yield* HttpRouter.HttpRouter + yield* router.add("*", "/*", (request) => serveUIEffect(request, { fs, client })) + }), ).pipe( Layer.provide(authorizationRouterMiddleware.layer.pipe(Layer.provide(ServerAuthConfig.defaultLayer))), - Layer.provide(HttpServer.layerServices), - Layer.provide( + Layer.provide([ + AppFileSystem.defaultLayer, + input?.client ?? httpClient(new Response("ui")), + HttpServer.layerServices, ConfigProvider.layer( ConfigProvider.fromUnknown({ OPENCODE_SERVER_PASSWORD: input?.password, OPENCODE_SERVER_USERNAME: input?.username, }), ), - ), + ]), ), { disableLogger: true }, ).handler @@ -140,26 +144,32 @@ describe("HttpApi UI fallback", () => { let proxiedUrl: string | undefined const response = await Effect.runPromise( - serveUIEffect(HttpServerRequest.fromWeb(new Request("http://localhost/assets/app.js"))).pipe( - Effect.provide(AppFileSystem.defaultLayer), + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const client = yield* HttpClient.HttpClient + return yield* serveUIEffect(HttpServerRequest.fromWeb(new Request("http://localhost/assets/app.js")), { fs, client }) + }).pipe( Effect.provide( - Layer.succeed( - HttpClient.HttpClient, - HttpClient.make((request) => { - proxiedUrl = request.url - return Effect.succeed( - HttpClientResponse.fromWeb( - request, - new Response("console.log('ok')", { - headers: { - "content-encoding": "br", - "content-length": "999", - "content-type": "text/javascript", - }, - }), - ), - ) - }), + Layer.mergeAll( + AppFileSystem.defaultLayer, + Layer.succeed( + HttpClient.HttpClient, + HttpClient.make((request) => { + proxiedUrl = request.url + return Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response("console.log('ok')", { + headers: { + "content-encoding": "br", + "content-length": "999", + "content-type": "text/javascript", + }, + }), + ), + ) + }), + ), ), ), Effect.map(HttpServerResponse.toWeb),