diff --git a/packages/desktop-electron/src/main/index.ts b/packages/desktop-electron/src/main/index.ts index c9f16606aeda..21abac772544 100644 --- a/packages/desktop-electron/src/main/index.ts +++ b/packages/desktop-electron/src/main/index.ts @@ -4,6 +4,7 @@ import { existsSync } from "node:fs" import { createServer } from "node:net" import { homedir } from "node:os" import { join } from "node:path" +import { parseArgs } from "node:util" import type { Event } from "electron" import { app, BrowserWindow, dialog } from "electron" import pkg from "electron-updater" @@ -63,6 +64,7 @@ const pendingDeepLinks: string[] = [] const serverReady = defer() const logger = initLogging() +const remoteServerUrl = getRemoteServerUrl() logger.log("app starting", { version: app.getVersion(), @@ -138,6 +140,25 @@ function setInitStep(step: InitStep) { } async function initialize() { + const overlay = remoteServerUrl ? initializeRemoteServer(remoteServerUrl) : await initializeSidecarServer() + mainWindow = createMainWindow() + wireMenu() + overlay?.close() +} + +function initializeRemoteServer(url: string) { + logger.log("using remote server", { url }) + serverReady.resolve({ + url, + username: null, + password: null, + source: "remote", + }) + setInitStep({ phase: "done" }) + return null +} + +async function initializeSidecarServer() { const needsMigration = !sqliteFileExists() const sqliteDone = needsMigration ? defer() : undefined let overlay: BrowserWindow | null = null @@ -181,6 +202,7 @@ async function initialize() { url, username: "opencode", password, + source: "sidecar", }) await Promise.race([ @@ -210,10 +232,7 @@ async function initialize() { await loadingComplete.promise } - mainWindow = createMainWindow() - wireMenu() - - overlay?.close() + return overlay } function wireMenu() { @@ -292,6 +311,34 @@ function ensureLoopbackNoProxy() { upsert("no_proxy") } +function getRemoteServerUrl() { + const parsed = parseLaunchArgs().values["server-url"] + const value = typeof parsed === "string" ? parsed : process.env.OPENCODE_DESKTOP_SERVER_URL + if (!value) return + + try { + const url = new URL(value) + if (url.protocol !== "http:" && url.protocol !== "https:") return + url.pathname = url.pathname.replace(/\/+$/, "") + return url.toString().replace(/\/$/, "") + } catch { + logger.warn("ignoring invalid --server-url", { value }) + } +} + +function parseLaunchArgs() { + return parseArgs({ + args: process.argv.slice(1), + options: { + "server-url": { + type: "string", + }, + }, + strict: false, + allowPositionals: true, + }) +} + async function getSidecarPort() { const fromEnv = process.env.OPENCODE_PORT if (fromEnv) { diff --git a/packages/desktop-electron/src/main/windows.ts b/packages/desktop-electron/src/main/windows.ts index 337e1ca0bcc4..99a62cfb29e5 100644 --- a/packages/desktop-electron/src/main/windows.ts +++ b/packages/desktop-electron/src/main/windows.ts @@ -110,6 +110,7 @@ export function createMainWindow() { const { responseHeaders = {} } = details upsertKeyValue(responseHeaders, "Access-Control-Allow-Origin", ["*"]) upsertKeyValue(responseHeaders, "Access-Control-Allow-Headers", ["*"]) + upsertKeyValue(responseHeaders, "Access-Control-Allow-Methods", ["GET, POST, PUT, PATCH, DELETE, OPTIONS"]) callback({ responseHeaders }) }) diff --git a/packages/desktop-electron/src/preload/types.ts b/packages/desktop-electron/src/preload/types.ts index 6e22954d18fa..21bed23d81b5 100644 --- a/packages/desktop-electron/src/preload/types.ts +++ b/packages/desktop-electron/src/preload/types.ts @@ -4,6 +4,7 @@ export type ServerReadyData = { url: string username: string | null password: string | null + source: "sidecar" | "remote" } export type SqliteMigrationProgress = { type: "InProgress"; value: number } | { type: "Done" } diff --git a/packages/desktop-electron/src/renderer/index.tsx b/packages/desktop-electron/src/renderer/index.tsx index 91ea1ae0772e..a31836c254ed 100644 --- a/packages/desktop-electron/src/renderer/index.tsx +++ b/packages/desktop-electron/src/renderer/index.tsx @@ -268,8 +268,7 @@ render(() => { const [windowCount] = createResource(() => window.api.getWindowCount()) - // Fetch sidecar credentials (available immediately, before health check) - const [sidecar] = createResource(() => window.api.awaitInitialization(() => undefined)) + const [launchServer] = createResource(() => window.api.awaitInitialization(() => undefined)) const [defaultServer] = createResource(() => platform.getDefaultServer?.().then((url) => { @@ -279,8 +278,18 @@ render(() => { const [locale] = createResource(loadLocale) const servers = () => { - const data = sidecar() + const data = launchServer() if (!data) return [] + if (data.source === "remote") { + return [ + { + type: "http", + http: { + url: data.url, + }, + }, + ] as ServerConnection.Any[] + } const server: ServerConnection.Sidecar = { displayName: "Local Server", type: "sidecar", @@ -333,7 +342,7 @@ render(() => { { {(_) => { return (