diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 11216643e5b..4ab22f21422 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -35,10 +35,12 @@ const url = iife(() => { if (location.hostname.includes("opencode.ai")) return "http://localhost:4096" if (window.__OPENCODE__) return `http://127.0.0.1:${window.__OPENCODE__.port}` - if (import.meta.env.DEV) - return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}` - return "http://localhost:4096" + const host = import.meta.env.VITE_OPENCODE_SERVER_HOST + const port = import.meta.env.VITE_OPENCODE_SERVER_PORT + if (host || port) return `http://${host ?? "localhost"}:${port ?? "4096"}` + + return window.location.origin }) export function App() { diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts index 3d3036b1b07..8840f373bb0 100644 --- a/packages/opencode/src/cli/cmd/web.ts +++ b/packages/opencode/src/cli/cmd/web.ts @@ -40,14 +40,20 @@ export const WebCommand = cmd({ type: "string", describe: "hostname to listen on", default: "127.0.0.1", + }) + .option("app-dir", { + type: "string", + describe: "serve local app assets from this directory instead of proxying", }), describe: "starts a headless opencode server", handler: async (args) => { const hostname = args.hostname const port = args.port + const appDir = args.appDir const server = Server.listen({ port, hostname, + appDir, }) UI.empty() UI.println(UI.logo(" ")) diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index c74dbbb41ef..47b33cdb22a 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -48,12 +48,14 @@ import { upgradeWebSocket, websocket } from "hono/bun" import { errors } from "./error" import { Pty } from "@/pty" import { Installation } from "@/installation" +import { join } from "path" // @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85 globalThis.AI_SDK_LOG_WARNINGS = false export namespace Server { const log = Log.create({ service: "server" }) + let appDir: string | undefined export const Event = { Connected: BusEvent.define("server.connected", z.object({})), @@ -2600,6 +2602,23 @@ export namespace Server { }, ) .all("/*", async (c) => { + if (appDir) { + const path = c.req.path === "/" ? "/index.html" : c.req.path + const file = Bun.file(join(appDir, path)) + if (await file.exists()) { + return new Response(await file.arrayBuffer(), { + headers: { "Content-Type": file.type }, + }) + } + // SPA fallback - serve index.html for client-side routing + const index = Bun.file(join(appDir, "index.html")) + if (await index.exists()) { + return new Response(await index.arrayBuffer(), { + headers: { "Content-Type": index.type }, + }) + } + return c.notFound() + } return proxy(`https://app.opencode.ai${c.req.path}`, { ...c.req, headers: { @@ -2623,7 +2642,10 @@ export namespace Server { return result } - export function listen(opts: { port: number; hostname: string }) { + export function listen(opts: { port: number; hostname: string; appDir?: string }) { + if (opts.appDir) { + appDir = opts.appDir + } const args = { hostname: opts.hostname, idleTimeout: 0,