Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/app/src/context/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,21 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
})
})

createEffect(() => {
if (!serverSync().ready) return
if (server.projects.list().length > 0) return
const serverProjects = serverSync().data.project
if (serverProjects.length === 0) return
batch(() => {
for (const p of serverProjects) {
if (p.worktree && p.id !== "global" && p.worktree !== "/") {
server.projects.open(p.worktree)
void serverSync().project.loadSessions(p.worktree)
}
}
})
})

const enriched = createMemo(() => server.projects.list().map(enrich))
const list = createMemo(() => {
const projects = enriched()
Expand Down
6 changes: 5 additions & 1 deletion packages/app/src/entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,12 @@ if (!(root instanceof HTMLElement) && import.meta.env.DEV) {

const getCurrentUrl = () => {
if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
if (import.meta.env.DEV)
if (import.meta.env.DEV) {
if (location.port && !["3000", "3001", "3002", "5173"].includes(location.port)) {
return location.origin
}
return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
}
return location.origin
}

Expand Down
60 changes: 58 additions & 2 deletions packages/app/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,68 @@ const sentry =
})
: false

// lru_map is a UMD bundle. When @pierre/diffs is excluded from optimizeDeps,
// Vite serves lru_map as a raw file. The transform hook wraps it in a CJS
// context so the UMD's exports.LRUMap path fires, then re-exports as ESM.
import { readFileSync } from "node:fs"
const lruMapPlugin = {
name: "lru-map-interop",
enforce: "pre" as const,
transform(code: string, id: string) {
const cleanId = id.split("?")[0]
if (cleanId.includes("lru_map") && cleanId.endsWith("lru.js")) {
return {
code: [
`const __lruMod = { exports: {} };`,
`(function(module, exports) {`,
code,
`})(__lruMod, __lruMod.exports);`,
`export const LRUMap = __lruMod.exports.LRUMap;`,
`export default __lruMod.exports;`,
].join("\n"),
map: null,
}
}
},
}

import path from "node:path"

// session-ui uses `?worker&url` to import @pierre/diffs worker files.
// Vite's worker transform doesn't apply to workspace packages served as
// node_modules. This plugin intercepts those imports, resolves the worker
// file's absolute path, and exports its /@fs/ URL so new Worker(...) works.
const workerUrlPlugin = {
name: "worker-url-interop",
enforce: "pre" as const,
resolveId(id: string, importer: string | undefined) {
if (id.includes("?worker") && importer) {
const cleanId = id.split("?")[0]
const absPath = cleanId.startsWith(".")
? path.resolve(path.dirname(importer.split("?")[0]), cleanId)
: cleanId
return "\0worker-url:" + absPath
}
},
load(id: string) {
if (id.startsWith("\0worker-url:")) {
const absPath = id.slice("\0worker-url:".length)
return `export default "/@fs${absPath}";`
}
},
}

export default defineConfig({
plugins: [desktopPlugin, sentry] as any,
plugins: [lruMapPlugin, workerUrlPlugin, desktopPlugin, sentry] as any,
server: {
host: "0.0.0.0",
allowedHosts: true,
port: 3000,
cors: true,
port: 3002,
strictPort: true,
},
optimizeDeps: {
exclude: ["@pierre/diffs"],
},
build: {
target: "esnext",
Expand Down
2 changes: 2 additions & 0 deletions packages/opencode/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ app.log
script/build-*.ts
temporary-*.md
.artifacts
opencode-web-ui.gen.ts

36 changes: 32 additions & 4 deletions packages/opencode/src/server/shared/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,34 @@ import { HttpBody, HttpClient, HttpClientRequest, HttpServerRequest, HttpServerR
import { createHash } from "node:crypto"
import { ProxyUtil } from "../proxy-util"

import path from "node:path"
import fs from "node:fs"

let embeddedUIPromise: Promise<Record<string, string> | null> | undefined

export const UI_UPSTREAM = new URL("https://app.opencode.ai")
export const UI_UPSTREAM = new URL(process.env.UI_UPSTREAM || "https://app.opencode.ai")

function getLocalWebUI(): Record<string, string> | null {
const distDir = path.resolve(import.meta.dirname, "../../../../app/dist")
if (!fs.existsSync(distDir)) return null

const map: Record<string, string> = {}
function walk(dir: string) {
const list = fs.readdirSync(dir)
for (const file of list) {
const fullPath = path.join(dir, file)
const stat = fs.statSync(fullPath)
if (stat.isDirectory()) {
walk(fullPath)
} else {
const rel = path.relative(distDir, fullPath).replaceAll("\\", "/")
map[rel] = fullPath
}
}
}
walk(distDir)
return map
}

export const csp = (hash = "") =>
`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:`
Expand Down Expand Up @@ -45,7 +70,9 @@ 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))
import("opencode-web-ui.gen.ts")
.then((module) => module.default as Record<string, string>)
.catch(() => getLocalWebUI()))
}

function notFound() {
Expand Down Expand Up @@ -81,12 +108,13 @@ export function serveUIEffect(
) {
return Effect.gen(function* () {
const embeddedWebUI = yield* Effect.promise(() => embeddedUI(services.disableEmbeddedWebUi))
const path = new URL(request.url, "http://localhost").pathname
const parsedUrl = new URL(request.url, "http://localhost")
const path = parsedUrl.pathname

if (embeddedWebUI) return yield* serveEmbeddedUIEffect(path, services.fs, embeddedWebUI)

const response = yield* services.client.execute(
HttpClientRequest.make(request.method)(upstreamURL(path), {
HttpClientRequest.make(request.method)(upstreamURL(path + parsedUrl.search), {
headers: ProxyUtil.headers(request.headers, { host: UI_UPSTREAM.host }),
body: requestBody(request),
}),
Expand Down
Loading