Skip to content

Commit dd426fa

Browse files
Zexiclaude
authored andcommitted
feat: serve local web UI in dev mode, suppress auth dialog
- Use Bearer WWW-Authenticate scheme so browsers don't pop a native credentials dialog on 401 (Chrome/Firefox do this for Basic but not Bearer). The server still accepts Authorization: Basic from the desktop app. - Add OPENCODE_DEV_UI_DIR flag: when set, the server serves static files from that directory (SPA fallback to index.html) instead of proxying to app.opencode.ai. - In desktop dev mode (!app.isPackaged), automatically set OPENCODE_DEV_UI_DIR to packages/app/dist so remote browsers see the local build. - Add OPENCODE_DEV_UI_URL flag to override the upstream proxy base URL. - Fix variable shadowing: rename path→requestPath in serveUIEffect to avoid conflict with the `import path from "node:path"` added in a prior commit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 02416fa commit dd426fa

4 files changed

Lines changed: 32 additions & 5 deletions

File tree

packages/core/src/flag/flag.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ export const Flag = {
7777
OPENCODE_MODELS_URL: process.env["OPENCODE_MODELS_URL"],
7878
OPENCODE_MODELS_PATH: process.env["OPENCODE_MODELS_PATH"],
7979
OPENCODE_DISABLE_EMBEDDED_WEB_UI: truthy("OPENCODE_DISABLE_EMBEDDED_WEB_UI"),
80+
OPENCODE_DEV_UI_URL: process.env["OPENCODE_DEV_UI_URL"],
81+
OPENCODE_DEV_UI_DIR: process.env["OPENCODE_DEV_UI_DIR"],
8082
OPENCODE_DB: process.env["OPENCODE_DB"],
8183
OPENCODE_DISABLE_CHANNEL_DB: truthy("OPENCODE_DISABLE_CHANNEL_DB"),
8284
OPENCODE_SKIP_MIGRATIONS: truthy("OPENCODE_SKIP_MIGRATIONS"),

packages/desktop/src/main/server.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ export function preferAppEnv(userDataPath: string) {
100100
OPENCODE_EXPERIMENTAL_FILEWATCHER: "true",
101101
OPENCODE_CLIENT: "desktop",
102102
XDG_STATE_HOME: process.env.XDG_STATE_HOME ?? userDataPath,
103+
// In dev mode, serve the web UI from the local build output so remote
104+
// browsers see the current local build instead of app.opencode.ai.
105+
...(!app.isPackaged ? { OPENCODE_DEV_UI_DIR: join(dirname(fileURLToPath(import.meta.url)), "../../../app/dist") } : {}),
103106
})
104107
}
105108

packages/opencode/src/server/routes/instance/httpapi/middleware/authorization.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import { isPublicUIPath } from "@/server/shared/public-ui"
77

88
const AUTH_TOKEN_QUERY = "auth_token"
99
const UNAUTHORIZED = 401
10-
const WWW_AUTHENTICATE = 'Basic realm="Secure Area"'
10+
// Use Bearer scheme so browsers don't show a native auth dialog on 401.
11+
// The server still accepts Authorization: Basic credentials from the app.
12+
const WWW_AUTHENTICATE = 'Bearer realm="Secure Area"'
1113

1214
// Avoid HttpApiSecurity alternatives here: Effect security middleware wraps the
1315
// full handler, so a downstream failure can make the next auth alternative run

packages/opencode/src/server/shared/ui.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const embeddedUIPromise = Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI
1717
return null
1818
})
1919

20-
export const UI_UPSTREAM = new URL("https://app.opencode.ai")
20+
export const UI_UPSTREAM = new URL(Flag.OPENCODE_DEV_UI_URL ?? "https://app.opencode.ai")
2121

2222
export const csp = (hash = "") =>
2323
`default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src * data:`
@@ -95,18 +95,38 @@ export function serveEmbeddedUIEffect(
9595
)
9696
}
9797

98+
function serveLocalDirEffect(
99+
requestPath: string,
100+
fs: AppFileSystem.Interface,
101+
dir: string,
102+
) {
103+
const filePath = path.join(dir, requestPath === "/" ? "index.html" : requestPath)
104+
return fs.readFile(filePath).pipe(
105+
Effect.map((body) => embeddedUIResponse(filePath, body)),
106+
Effect.catchReason("PlatformError", "NotFound", () =>
107+
fs.readFile(path.join(dir, "index.html")).pipe(
108+
Effect.map((body) => embeddedUIResponse(path.join(dir, "index.html"), body)),
109+
Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(notFound())),
110+
),
111+
),
112+
)
113+
}
114+
98115
export function serveUIEffect(
99116
request: HttpServerRequest.HttpServerRequest,
100117
services: { fs: AppFileSystem.Interface; client: HttpClient.HttpClient },
101118
) {
102119
return Effect.gen(function* () {
103120
const embeddedWebUI = yield* Effect.promise(() => embeddedUI())
104-
const path = new URL(request.url, "http://localhost").pathname
121+
const requestPath = new URL(request.url, "http://localhost").pathname
122+
123+
if (embeddedWebUI) return yield* serveEmbeddedUIEffect(requestPath, services.fs, embeddedWebUI)
105124

106-
if (embeddedWebUI) return yield* serveEmbeddedUIEffect(path, services.fs, embeddedWebUI)
125+
// Dev mode: serve from local build directory if configured
126+
if (Flag.OPENCODE_DEV_UI_DIR) return yield* serveLocalDirEffect(requestPath, services.fs, Flag.OPENCODE_DEV_UI_DIR)
107127

108128
const response = yield* services.client.execute(
109-
HttpClientRequest.make(request.method)(upstreamURL(path), {
129+
HttpClientRequest.make(request.method)(upstreamURL(requestPath), {
110130
headers: ProxyUtil.headers(request.headers, { host: UI_UPSTREAM.host }),
111131
body: requestBody(request),
112132
}),

0 commit comments

Comments
 (0)