summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorBrendan Allan <[email protected]>2026-04-21 12:36:56 +0800
committerGitHub <[email protected]>2026-04-21 12:36:56 +0800
commite5687d646ce33b5c05bb007bf14cf5362676733b (patch)
tree6e0911ea079611e650806abbd1135f36e2421642 /packages
parenta38d53fe2f056e55347861b87f349264e7abec48 (diff)
downloadopencode-e5687d646ce33b5c05bb007bf14cf5362676733b.tar.gz
opencode-e5687d646ce33b5c05bb007bf14cf5362676733b.zip
electron: use custom oc:// protocol for renderer windows (#23516)
Diffstat (limited to 'packages')
-rw-r--r--packages/desktop-electron/src/main/index.ts3
-rw-r--r--packages/desktop-electron/src/main/server.ts1
-rw-r--r--packages/desktop-electron/src/main/windows.ts41
-rw-r--r--packages/desktop-electron/src/renderer/html.test.ts6
4 files changed, 43 insertions, 8 deletions
diff --git a/packages/desktop-electron/src/main/index.ts b/packages/desktop-electron/src/main/index.ts
index 6c4e6d5ca..8ec39c3c9 100644
--- a/packages/desktop-electron/src/main/index.ts
+++ b/packages/desktop-electron/src/main/index.ts
@@ -42,7 +42,7 @@ import { initLogging } from "./logging"
import { parseMarkdown } from "./markdown"
import { createMenu } from "./menu"
import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server"
-import { createLoadingWindow, createMainWindow, setBackgroundColor, setDockIcon } from "./windows"
+import { createLoadingWindow, createMainWindow, registerRendererProtocol, setBackgroundColor, setDockIcon } from "./windows"
import { drizzle } from "drizzle-orm/node-sqlite/driver"
import type { Server } from "virtual:opencode-server"
@@ -106,6 +106,7 @@ function setupApp() {
void app.whenReady().then(async () => {
app.setAsDefaultProtocolClient("opencode")
+ registerRendererProtocol()
setDockIcon()
setupAutoUpdater()
await initialize()
diff --git a/packages/desktop-electron/src/main/server.ts b/packages/desktop-electron/src/main/server.ts
index 55dfdf6e9..83d50f7cb 100644
--- a/packages/desktop-electron/src/main/server.ts
+++ b/packages/desktop-electron/src/main/server.ts
@@ -39,6 +39,7 @@ export async function spawnLocalServer(hostname: string, port: number, password:
hostname,
username: "opencode",
password,
+ cors: ["oc://renderer"],
})
const wait = (async () => {
diff --git a/packages/desktop-electron/src/main/windows.ts b/packages/desktop-electron/src/main/windows.ts
index 95f80c124..892e9d40d 100644
--- a/packages/desktop-electron/src/main/windows.ts
+++ b/packages/desktop-electron/src/main/windows.ts
@@ -1,7 +1,7 @@
import windowState from "electron-window-state"
-import { app, BrowserWindow, nativeImage, nativeTheme } from "electron"
-import { dirname, join } from "node:path"
-import { fileURLToPath } from "node:url"
+import { app, BrowserWindow, net, nativeImage, nativeTheme, protocol } from "electron"
+import { dirname, isAbsolute, join, relative, resolve } from "node:path"
+import { fileURLToPath, pathToFileURL } from "node:url"
import type { TitlebarTheme } from "../preload/types"
type Globals = {
@@ -10,6 +10,20 @@ type Globals = {
}
const root = dirname(fileURLToPath(import.meta.url))
+const rendererRoot = join(root, "../renderer")
+const rendererProtocol = "oc"
+const rendererHost = "renderer"
+
+protocol.registerSchemesAsPrivileged([
+ {
+ scheme: rendererProtocol,
+ privileges: {
+ secure: true,
+ standard: true,
+ supportFetchAPI: true,
+ },
+ },
+])
let backgroundColor: string | undefined
@@ -131,6 +145,25 @@ export function createLoadingWindow(globals: Globals) {
return win
}
+export function registerRendererProtocol() {
+ if (protocol.isProtocolHandled(rendererProtocol)) return
+
+ protocol.handle(rendererProtocol, (request) => {
+ const url = new URL(request.url)
+ if (url.host !== rendererHost) {
+ return new Response("Not found", { status: 404 })
+ }
+
+ const file = resolve(rendererRoot, `.${decodeURIComponent(url.pathname)}`)
+ const rel = relative(rendererRoot, file)
+ if (rel.startsWith("..") || isAbsolute(rel)) {
+ return new Response("Not found", { status: 404 })
+ }
+
+ return net.fetch(pathToFileURL(file).toString())
+ })
+}
+
function loadWindow(win: BrowserWindow, html: string) {
const devUrl = process.env.ELECTRON_RENDERER_URL
if (devUrl) {
@@ -139,7 +172,7 @@ function loadWindow(win: BrowserWindow, html: string) {
return
}
- void win.loadFile(join(root, `../renderer/${html}`))
+ void win.loadURL(`${rendererProtocol}://${rendererHost}/${html}`)
}
function injectGlobals(win: BrowserWindow, globals: Globals) {
diff --git a/packages/desktop-electron/src/renderer/html.test.ts b/packages/desktop-electron/src/renderer/html.test.ts
index bd8281c2f..1fc5c8717 100644
--- a/packages/desktop-electron/src/renderer/html.test.ts
+++ b/packages/desktop-electron/src/renderer/html.test.ts
@@ -9,9 +9,9 @@ const root = resolve(dir, "../..")
const html = async (name: string) => Bun.file(join(dir, name)).text()
/**
- * Electron loads renderer HTML via `win.loadFile()` which uses the `file://`
- * protocol. Absolute paths like `src="/foo.js"` resolve to the filesystem root
- * (e.g. `file:///C:/foo.js` on Windows) instead of relative to the app bundle.
+ * Packaged Electron windows load renderer HTML via the privileged `oc://`
+ * protocol. Root-relative asset paths like `src="/foo.js"` would resolve from
+ * the protocol origin root instead of relative to the current HTML entrypoint.
*
* All local resource references must use relative paths (`./`).
*/