Explorar o código

refactor(flags): move embedded web ui flag to runtime flags (#27613)

Shoubhit Dash hai 3 meses
pai
achega
43310f4d8c

+ 0 - 1
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"),

+ 1 - 0
packages/opencode/src/effect/runtime-flags.ts

@@ -14,6 +14,7 @@ const enabledByExperimental = (name: string) =>
 export class Service extends ConfigService.Service<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"),

+ 6 - 1
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,

+ 8 - 10
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<string, string>).catch(() => null)
+let embeddedUIPromise: Promise<Record<string, string> | 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<string, string>).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)

+ 4 - 0
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)

+ 110 - 50
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<HttpClient.HttpClient> }) {
+function uiApp(input?: {
+  password?: string
+  username?: string
+  client?: Layer.Layer<HttpClient.HttpClient>
+  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("<html>opencode</html>", { 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("<html>opencode</html>", {
-                    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("<html>opencode</html>", {
+                      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("<html>opencode</html>", { 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)