From 43310f4d8cf87297ddbea2498d2e2a046a9a4ba0 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Fri, 15 May 2026 03:51:29 +0530 Subject: [PATCH] refactor(flags): move embedded web ui flag to runtime flags (#27613) --- packages/core/src/flag/flag.ts | 1 - packages/opencode/src/effect/runtime-flags.ts | 1 + .../server/routes/instance/httpapi/server.ts | 7 +- packages/opencode/src/server/shared/ui.ts | 18 +- .../test/effect/runtime-flags.test.ts | 4 + .../opencode/test/server/httpapi-ui.test.ts | 160 ++++++++++++------ 6 files changed, 129 insertions(+), 62 deletions(-) diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index 8b1f1609c6..f97c6953aa 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -62,7 +62,6 @@ export const Flag = { OPENCODE_ENABLE_PARALLEL: truthy("OPENCODE_ENABLE_PARALLEL") || truthy("OPENCODE_EXPERIMENTAL_PARALLEL"), OPENCODE_MODELS_URL: process.env["OPENCODE_MODELS_URL"], OPENCODE_MODELS_PATH: process.env["OPENCODE_MODELS_PATH"], - OPENCODE_DISABLE_EMBEDDED_WEB_UI: truthy("OPENCODE_DISABLE_EMBEDDED_WEB_UI"), OPENCODE_DB: process.env["OPENCODE_DB"], OPENCODE_DISABLE_CHANNEL_DB: truthy("OPENCODE_DISABLE_CHANNEL_DB"), OPENCODE_SKIP_MIGRATIONS: truthy("OPENCODE_SKIP_MIGRATIONS"), diff --git a/packages/opencode/src/effect/runtime-flags.ts b/packages/opencode/src/effect/runtime-flags.ts index 91f8205588..0c21b30e68 100644 --- a/packages/opencode/src/effect/runtime-flags.ts +++ b/packages/opencode/src/effect/runtime-flags.ts @@ -14,6 +14,7 @@ const enabledByExperimental = (name: string) => export class Service extends ConfigService.Service()("@opencode/RuntimeFlags", { pure: bool("OPENCODE_PURE"), disableDefaultPlugins: bool("OPENCODE_DISABLE_DEFAULT_PLUGINS"), + disableEmbeddedWebUi: bool("OPENCODE_DISABLE_EMBEDDED_WEB_UI"), disableClaudeCodeSkills: Config.all({ broad: bool("OPENCODE_DISABLE_CLAUDE_CODE"), direct: bool("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS"), diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts index cac6351325..4fdfd3a14b 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts @@ -21,6 +21,7 @@ import { File } from "@/file" import { FileWatcher } from "@/file/watcher" import { Ripgrep } from "@/file/ripgrep" import { Format } from "@/format" +import { RuntimeFlags } from "@/effect/runtime-flags" import { LSP } from "@/lsp/lsp" import { MCP } from "@/mcp" import { Permission } from "@/permission" @@ -172,7 +173,10 @@ const uiRoute = HttpRouter.use((router) => Effect.gen(function* () { const fs = yield* AppFileSystem.Service const client = yield* HttpClient.HttpClient - yield* router.add("*", "/*", (request) => serveUIEffect(request, { fs, client })) + const flags = yield* RuntimeFlags.Service + yield* router.add("*", "/*", (request) => + serveUIEffect(request, { fs, client, disableEmbeddedWebUi: flags.disableEmbeddedWebUi }), + ) }), ).pipe(Layer.provide(authOnlyRouterLayer)) @@ -206,6 +210,7 @@ export function createRoutes(corsOptions?: CorsOptions) { PtyTicket.defaultLayer, Question.defaultLayer, Ripgrep.defaultLayer, + RuntimeFlags.defaultLayer, Session.defaultLayer, SessionCompaction.defaultLayer, SessionPrompt.defaultLayer, diff --git a/packages/opencode/src/server/shared/ui.ts b/packages/opencode/src/server/shared/ui.ts index 0e27dcf220..fd4c731880 100644 --- a/packages/opencode/src/server/shared/ui.ts +++ b/packages/opencode/src/server/shared/ui.ts @@ -1,14 +1,10 @@ -import { Flag } from "@opencode-ai/core/flag/flag" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Effect, Stream } from "effect" import { HttpBody, HttpClient, HttpClientRequest, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" import { createHash } from "node:crypto" import { ProxyUtil } from "../proxy-util" -const embeddedUIPromise = Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI - ? Promise.resolve(null) - : // @ts-expect-error - generated file at build time - import("opencode-web-ui.gen.ts").then((module) => module.default as Record).catch(() => null) +let embeddedUIPromise: Promise | null> | undefined export const UI_UPSTREAM = new URL("https://app.opencode.ai") @@ -45,9 +41,11 @@ export function upstreamURL(path: string) { return new URL(path, UI_UPSTREAM).toString() } -export function embeddedUI() { - if (Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI) return Promise.resolve(null) - return embeddedUIPromise +export function embeddedUI(disableEmbeddedWebUi: boolean) { + if (disableEmbeddedWebUi) return Promise.resolve(null) + return (embeddedUIPromise ??= + // @ts-expect-error - generated file at build time + import("opencode-web-ui.gen.ts").then((module) => module.default as Record).catch(() => null)) } function notFound() { @@ -79,10 +77,10 @@ export function serveEmbeddedUIEffect( export function serveUIEffect( request: HttpServerRequest.HttpServerRequest, - services: { fs: AppFileSystem.Interface; client: HttpClient.HttpClient }, + services: { fs: AppFileSystem.Interface; client: HttpClient.HttpClient; disableEmbeddedWebUi: boolean }, ) { return Effect.gen(function* () { - const embeddedWebUI = yield* Effect.promise(() => embeddedUI()) + const embeddedWebUI = yield* Effect.promise(() => embeddedUI(services.disableEmbeddedWebUi)) const path = new URL(request.url, "http://localhost").pathname if (embeddedWebUI) return yield* serveEmbeddedUIEffect(path, services.fs, embeddedWebUI) diff --git a/packages/opencode/test/effect/runtime-flags.test.ts b/packages/opencode/test/effect/runtime-flags.test.ts index 8010038052..79c33e6dbf 100644 --- a/packages/opencode/test/effect/runtime-flags.test.ts +++ b/packages/opencode/test/effect/runtime-flags.test.ts @@ -16,6 +16,7 @@ describe("RuntimeFlags", () => { fromConfig({ OPENCODE_PURE: "true", OPENCODE_DISABLE_DEFAULT_PLUGINS: "true", + OPENCODE_DISABLE_EMBEDDED_WEB_UI: "true", OPENCODE_EXPERIMENTAL: "true", OPENCODE_ENABLE_EXA: "true", OPENCODE_ENABLE_PARALLEL: "true", @@ -28,6 +29,7 @@ describe("RuntimeFlags", () => { expect(flags.pure).toBe(true) expect(flags.disableDefaultPlugins).toBe(true) + expect(flags.disableEmbeddedWebUi).toBe(true) expect(flags.enableExa).toBe(true) expect(flags.enableParallel).toBe(true) expect(flags.enableExperimentalModels).toBe(true) @@ -67,6 +69,7 @@ describe("RuntimeFlags", () => { expect(flags.pure).toBe(false) expect(flags.disableDefaultPlugins).toBe(true) + expect(flags.disableEmbeddedWebUi).toBe(false) expect(flags.disableClaudeCodeSkills).toBe(false) expect(flags.enableExa).toBe(false) expect(flags.experimentalIconDiscovery).toBe(false) @@ -186,6 +189,7 @@ describe("RuntimeFlags", () => { expect(flags.pure).toBe(false) expect(flags.disableDefaultPlugins).toBe(false) + expect(flags.disableEmbeddedWebUi).toBe(false) expect(flags.disableClaudeCodeSkills).toBe(false) expect(flags.enableExa).toBe(false) expect(flags.experimentalIconDiscovery).toBe(false) diff --git a/packages/opencode/test/server/httpapi-ui.test.ts b/packages/opencode/test/server/httpapi-ui.test.ts index 74fc230042..3925e622ad 100644 --- a/packages/opencode/test/server/httpapi-ui.test.ts +++ b/packages/opencode/test/server/httpapi-ui.test.ts @@ -13,11 +13,11 @@ import { HttpServerResponse, } from "effect/unstable/http" import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { RuntimeFlags } from "../../src/effect/runtime-flags" 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 { serveEmbeddedUIEffect, serveUIEffect } from "../../src/server/shared/ui" -import { Server } from "../../src/server/server" import { testEffect } from "../lib/effect" void Log.init({ print: false }) @@ -25,7 +25,6 @@ void Log.init({ print: false }) const testStateLayer = Layer.effectDiscard( Effect.gen(function* () { const original = { - OPENCODE_DISABLE_EMBEDDED_WEB_UI: Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI, OPENCODE_SERVER_PASSWORD: Flag.OPENCODE_SERVER_PASSWORD, OPENCODE_SERVER_USERNAME: Flag.OPENCODE_SERVER_USERNAME, envPassword: process.env.OPENCODE_SERVER_PASSWORD, @@ -34,7 +33,6 @@ const testStateLayer = Layer.effectDiscard( yield* Effect.addFinalizer(() => Effect.sync(() => { - Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = original.OPENCODE_DISABLE_EMBEDDED_WEB_UI Flag.OPENCODE_SERVER_PASSWORD = original.OPENCODE_SERVER_PASSWORD Flag.OPENCODE_SERVER_USERNAME = original.OPENCODE_SERVER_USERNAME restoreEnv("OPENCODE_SERVER_PASSWORD", original.envPassword) @@ -44,7 +42,7 @@ const testStateLayer = Layer.effectDiscard( }), ) -const it = testEffect(Layer.mergeAll(testStateLayer, AppFileSystem.defaultLayer)) +const it = testEffect(Layer.mergeAll(testStateLayer, AppFileSystem.defaultLayer, RuntimeFlags.layer())) function restoreEnv(key: string, value: string | undefined) { if (value === undefined) { @@ -82,19 +80,28 @@ function app(input?: { password?: string; username?: string }) { } } -function uiApp(input?: { password?: string; username?: string; client?: Layer.Layer }) { +function uiApp(input?: { + password?: string + username?: string + client?: Layer.Layer + disableEmbeddedWebUi?: boolean +}) { const handler = HttpRouter.toWebHandler( HttpRouter.use((router) => Effect.gen(function* () { const fs = yield* AppFileSystem.Service const client = yield* HttpClient.HttpClient - yield* router.add("*", "/*", (request) => serveUIEffect(request, { fs, client })) + const flags = yield* RuntimeFlags.Service + yield* router.add("*", "/*", (request) => + serveUIEffect(request, { fs, client, disableEmbeddedWebUi: flags.disableEmbeddedWebUi }), + ) }), ).pipe( Layer.provide(authorizationRouterMiddleware.layer.pipe(Layer.provide(ServerAuth.Config.defaultLayer))), Layer.provide([ AppFileSystem.defaultLayer, input?.client ?? httpClient(new Response("ui")), + RuntimeFlags.layer({ disableEmbeddedWebUi: input?.disableEmbeddedWebUi ?? false }), HttpServer.layerServices, ConfigProvider.layer( ConfigProvider.fromUnknown({ @@ -120,6 +127,48 @@ function uiApp(input?: { password?: string; username?: string; client?: Layer.La } } +function routeOrderingApp() { + let proxiedUrl: string | undefined + const handler = HttpRouter.toWebHandler( + HttpRouter.use((router) => + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const client = yield* HttpClient.HttpClient + const flags = yield* RuntimeFlags.Service + yield* router.add("GET", "/session/:sessionID", () => + Effect.succeed(HttpServerResponse.jsonUnsafe({ error: "Not Found" }, { status: 404 })), + ) + yield* router.add("*", "/*", (request) => + serveUIEffect(request, { fs, client, disableEmbeddedWebUi: flags.disableEmbeddedWebUi }), + ) + }), + ).pipe( + Layer.provide([ + AppFileSystem.defaultLayer, + RuntimeFlags.layer({ disableEmbeddedWebUi: true }), + httpClient(new Response("ui"), (request) => { + proxiedUrl = request.url + }), + HttpServer.layerServices, + ]), + ), + { disableLogger: true }, + ).handler + return { + proxiedUrl: () => proxiedUrl, + request(input: string | URL | Request, init?: RequestInit) { + return Effect.promise(() => + Promise.resolve( + handler( + input instanceof Request ? input : new Request(new URL(input, "http://localhost"), init), + ExperimentalHttpApiServer.context, + ), + ), + ) + }, + } +} + function httpClient(response: Response, onRequest?: (request: HttpClientRequest.HttpClientRequest) => void) { return Layer.succeed( HttpClient.HttpClient, @@ -137,10 +186,10 @@ function responseText(response: Response) { describe("HttpApi UI fallback", () => { it.live("serves the web UI through the experimental backend", () => Effect.gen(function* () { - Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true let proxiedUrl: string | undefined const response = yield* uiApp({ + disableEmbeddedWebUi: true, client: httpClient( new Response("opencode", { headers: { "content-type": "text/html" } }), (request) => { @@ -158,35 +207,39 @@ describe("HttpApi UI fallback", () => { it.live("strips upstream transfer encoding headers from proxied assets", () => Effect.gen(function* () { - Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true let proxiedUrl: string | undefined const response = yield* Effect.gen(function* () { const fs = yield* AppFileSystem.Service const client = yield* HttpClient.HttpClient + const flags = yield* RuntimeFlags.Service return yield* serveUIEffect(HttpServerRequest.fromWeb(new Request("http://localhost/assets/app.js")), { fs, client, + disableEmbeddedWebUi: flags.disableEmbeddedWebUi, }) }).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( + RuntimeFlags.layer({ disableEmbeddedWebUi: true }), + 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), @@ -206,29 +259,32 @@ describe("HttpApi UI fallback", () => { // causing browsers to fail with `ERR_INVALID_CHUNKED_ENCODING`. it.live("strips upstream transfer-encoding header from proxied assets", () => Effect.gen(function* () { - Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true - const response = yield* Effect.gen(function* () { const fs = yield* AppFileSystem.Service const client = yield* HttpClient.HttpClient + const flags = yield* RuntimeFlags.Service return yield* serveUIEffect(HttpServerRequest.fromWeb(new Request("http://localhost/")), { fs, client, + disableEmbeddedWebUi: flags.disableEmbeddedWebUi, }) }).pipe( Effect.provide( - Layer.succeed( - HttpClient.HttpClient, - HttpClient.make((request) => - Effect.succeed( - HttpClientResponse.fromWeb( - request, - new Response("opencode", { - headers: { - "transfer-encoding": "chunked", - "content-type": "text/html", - }, - }), + Layer.mergeAll( + RuntimeFlags.layer({ disableEmbeddedWebUi: true }), + Layer.succeed( + HttpClient.HttpClient, + HttpClient.make((request) => + Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response("opencode", { + headers: { + "transfer-encoding": "chunked", + "content-type": "text/html", + }, + }), + ), ), ), ), @@ -301,17 +357,21 @@ describe("HttpApi UI fallback", () => { it.live("keeps matched API routes ahead of the UI fallback", () => Effect.gen(function* () { - const response = yield* Effect.promise(() => Promise.resolve(Server.Default().app.request("/session/ses_nope"))) + const server = routeOrderingApp() + const response = yield* server.request("/session/ses_nope") expect(response.status).toBe(404) + expect(server.proxiedUrl()).toBeUndefined() }), ) it.live("requires server password for the web UI", () => Effect.gen(function* () { - Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true - - const response = yield* uiApp({ password: "secret", username: "opencode" }).request("/") + const response = yield* uiApp({ + password: "secret", + username: "opencode", + disableEmbeddedWebUi: true, + }).request("/") expect(response.status).toBe(401) expect(response.headers.get("www-authenticate")).toBe('Basic realm="Secure Area"') @@ -320,11 +380,10 @@ describe("HttpApi UI fallback", () => { it.live("accepts auth token for the web UI", () => Effect.gen(function* () { - Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true - const response = yield* uiApp({ password: "secret", username: "opencode", + disableEmbeddedWebUi: true, client: httpClient(new Response("opencode", { headers: { "content-type": "text/html" } })), }).request(`/?auth_token=${btoa("opencode:secret")}`) @@ -335,9 +394,11 @@ describe("HttpApi UI fallback", () => { it.live("accepts basic auth for the web UI", () => Effect.gen(function* () { - Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true - - const response = yield* uiApp({ password: "secret", username: "opencode" }).request("/", { + const response = yield* uiApp({ + password: "secret", + username: "opencode", + disableEmbeddedWebUi: true, + }).request("/", { headers: { authorization: `Basic ${btoa("opencode:secret")}` }, }) @@ -352,12 +413,11 @@ describe("HttpApi UI fallback", () => { // should bypass auth. it.live("serves the PWA manifest without auth even when a server password is set", () => Effect.gen(function* () { - Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true - for (const path of ["/site.webmanifest", "/web-app-manifest-192x192.png", "/web-app-manifest-512x512.png"]) { const response = yield* uiApp({ password: "secret", username: "opencode", + disableEmbeddedWebUi: true, client: httpClient(new Response("ok")), }).request(path) expect(response.status).not.toBe(401)