diff --git a/packages/opencode/src/server/shared/ui.ts b/packages/opencode/src/server/shared/ui.ts index db67749e08..c1558a1a4e 100644 --- a/packages/opencode/src/server/shared/ui.ts +++ b/packages/opencode/src/server/shared/ui.ts @@ -45,6 +45,31 @@ export function embeddedUI() { return embeddedUIPromise } +function notFound() { + return HttpServerResponse.jsonUnsafe({ error: "Not Found" }, { status: 404 }) +} + +function embeddedUIResponse(file: string, body: Uint8Array) { + const mime = AppFileSystem.mimeType(file) + const headers = new Headers({ "content-type": mime }) + if (mime.startsWith("text/html")) headers.set("content-security-policy", DEFAULT_CSP) + return HttpServerResponse.raw(body, { headers }) +} + +export function serveEmbeddedUIEffect( + requestPath: string, + fs: AppFileSystem.Interface, + embeddedWebUI: Record, +) { + const file = embeddedWebUI[requestPath.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null + if (!file) return Effect.succeed(notFound()) + + return fs.readFile(file).pipe( + Effect.map((body) => embeddedUIResponse(file, body)), + Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(notFound())), + ) +} + export function serveUIEffect( request: HttpServerRequest.HttpServerRequest, services: { fs: AppFileSystem.Interface; client: HttpClient.HttpClient }, @@ -53,19 +78,7 @@ export function serveUIEffect( const embeddedWebUI = yield* Effect.promise(() => embeddedUI()) const path = new URL(request.url, "http://localhost").pathname - if (embeddedWebUI) { - const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null - if (!match) return HttpServerResponse.jsonUnsafe({ error: "Not Found" }, { status: 404 }) - - if (yield* services.fs.existsSafe(match)) { - const mime = AppFileSystem.mimeType(match) - const headers = new Headers({ "content-type": mime }) - if (mime.startsWith("text/html")) headers.set("content-security-policy", DEFAULT_CSP) - return HttpServerResponse.raw(yield* services.fs.readFile(match), { headers }) - } - - return HttpServerResponse.jsonUnsafe({ error: "Not Found" }, { status: 404 }) - } + if (embeddedWebUI) return yield* serveEmbeddedUIEffect(path, services.fs, embeddedWebUI) const response = yield* services.client.execute( HttpClientRequest.make(request.method)(upstreamURL(path), { diff --git a/packages/opencode/test/server/httpapi-ui.test.ts b/packages/opencode/test/server/httpapi-ui.test.ts index 8b7a6a1ac3..332ad16c64 100644 --- a/packages/opencode/test/server/httpapi-ui.test.ts +++ b/packages/opencode/test/server/httpapi-ui.test.ts @@ -15,7 +15,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem" import { ServerAuth } from "../../src/server/auth" import { authorizationRouterMiddleware } from "../../src/server/routes/instance/httpapi/middleware/authorization" import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server" -import { serveUIEffect } from "../../src/server/shared/ui" +import { serveEmbeddedUIEffect, serveUIEffect } from "../../src/server/shared/ui" import { Server } from "../../src/server/server" void Log.init({ print: false }) @@ -184,6 +184,39 @@ describe("HttpApi UI fallback", () => { expect(await response.text()).toBe("console.log('ok')") }) + test("serves embedded UI assets when Bun can read them but access reports missing", async () => { + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true + let readPath: string | undefined + + const response = await Effect.runPromise( + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + return yield* serveEmbeddedUIEffect( + "/assets/app.js", + { + ...fs, + existsSafe: () => Effect.die("embedded UI should not rely on filesystem access checks"), + readFile: (path) => { + readPath = path + return path === "/$bunfs/root/assets/app.js" + ? Effect.succeed(new TextEncoder().encode("console.log('embedded')")) + : Effect.die(`unexpected embedded UI path: ${path}`) + }, + }, + { "assets/app.js": "/$bunfs/root/assets/app.js" }, + ) + }).pipe( + Effect.provide(AppFileSystem.defaultLayer), + Effect.map(HttpServerResponse.toWeb), + ), + ) + + expect(response.status).toBe(200) + expect(readPath).toBe("/$bunfs/root/assets/app.js") + expect(response.headers.get("content-type")).toContain("text/javascript") + expect(await response.text()).toBe("console.log('embedded')") + }) + test("keeps matched API routes ahead of the UI fallback", async () => { Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true