diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index 4813ecc45d3..f42b2becc58 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -45,9 +45,9 @@ function useDefaultServer(platform: ReturnType, language: Re const [defaultUrl, defaultUrlActions] = createResource( async () => { try { - const url = await platform.getDefaultServerUrl?.() - if (!url) return null - return normalizeServerUrl(url) ?? null + const config = await platform.getDefaultServerUrl?.() + if (!config) return null + return config.url } catch (err) { showRequestError(language, err) return null @@ -57,10 +57,17 @@ function useDefaultServer(platform: ReturnType, language: Re ) const canDefault = createMemo(() => !!platform.getDefaultServerUrl && !!platform.setDefaultServerUrl) - const setDefault = async (url: string | null) => { + const setDefault = async (conn: ServerConnection.Http | null) => { try { - await platform.setDefaultServerUrl?.(url) - defaultUrlActions.mutate(url) + const config = conn + ? { + url: conn.http.url, + username: conn.http.username, + password: conn.http.password, + } + : null + await platform.setDefaultServerUrl?.(config) + defaultUrlActions.mutate(conn?.http.url ?? null) } catch (err) { showRequestError(language, err) } @@ -494,7 +501,8 @@ export function DialogSelectServer() { async function handleRemove(url: ServerConnection.Key) { server.remove(url) - if ((await platform.getDefaultServerUrl?.()) === url) { + const defaultConfig = await platform.getDefaultServerUrl?.() + if (defaultConfig?.url === url) { platform.setDefaultServerUrl?.(null) } } @@ -585,7 +593,12 @@ export function DialogSelectServer() { {language.t("dialog.server.menu.edit")} - setDefault(i.http.url)}> + { + if (i.type !== "http") return + setDefault(i) + }} + > {language.t("dialog.server.menu.default")} diff --git a/packages/app/src/components/status-popover.tsx b/packages/app/src/components/status-popover.tsx index c61b3195832..4c26dc330ee 100644 --- a/packages/app/src/components/status-popover.tsx +++ b/packages/app/src/components/status-popover.tsx @@ -83,7 +83,13 @@ const useServerHealth = (servers: Accessor, fetcher: typ } const useDefaultServerKey = ( - get: (() => string | Promise | null | undefined) | undefined, + get: + | (() => + | import("@/context/platform").DefaultServerConfig + | Promise + | null + | undefined) + | undefined, ) => { const [url, setUrl] = createSignal() const [tick, setTick] = createSignal(0) @@ -103,7 +109,7 @@ const useDefaultServerKey = ( if (result instanceof Promise) { void result.then((next) => { if (dead) return - setUrl(next ? normalizeServerUrl(next) : undefined) + setUrl(next?.url ? normalizeServerUrl(next.url) : undefined) }) onCleanup(() => { dead = true @@ -111,7 +117,7 @@ const useDefaultServerKey = ( return } - setUrl(normalizeServerUrl(result)) + setUrl(normalizeServerUrl(result.url)) onCleanup(() => { dead = true }) diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx index 86f3321e464..d2e118420e5 100644 --- a/packages/app/src/context/platform.tsx +++ b/packages/app/src/context/platform.tsx @@ -8,6 +8,12 @@ type OpenFilePickerOptions = { title?: string; multiple?: boolean } type SaveFilePickerOptions = { title?: string; defaultPath?: string } type UpdateInfo = { updateAvailable: boolean; version?: string } +export type DefaultServerConfig = { + url: string + username?: string + password?: string +} + export type Platform = { /** Platform discriminator */ platform: "web" | "desktop" @@ -58,10 +64,10 @@ export type Platform = { fetch?: typeof fetch /** Get the configured default server URL (platform-specific) */ - getDefaultServerUrl?(): Promise + getDefaultServerUrl?(): Promise /** Set the default server URL to use on app startup (platform-specific) */ - setDefaultServerUrl?(url: string | null): Promise | void + setDefaultServerUrl?(config: DefaultServerConfig | null): Promise | void /** Get the configured WSL integration (desktop only) */ getWslEnabled?(): Promise diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx index e9c0a4397c9..a4ca74bdebe 100644 --- a/packages/app/src/entry.tsx +++ b/packages/app/src/entry.tsx @@ -50,8 +50,25 @@ const setStorage = (key: string, value: string | null) => { } } -const readDefaultServerUrl = () => getStorage(DEFAULT_SERVER_URL_KEY) -const writeDefaultServerUrl = (url: string | null) => setStorage(DEFAULT_SERVER_URL_KEY, url) +const readDefaultServerUrl = () => { + const value = getStorage(DEFAULT_SERVER_URL_KEY) + if (!value) return null + try { + const parsed = JSON.parse(value) + if (typeof parsed === "object" && parsed && "url" in parsed) { + return parsed as import("@/context/platform").DefaultServerConfig + } + } catch {} + return { url: value } +} + +const writeDefaultServerUrl = (config: import("@/context/platform").DefaultServerConfig | null) => { + if (!config) { + setStorage(DEFAULT_SERVER_URL_KEY, null) + return + } + setStorage(DEFAULT_SERVER_URL_KEY, JSON.stringify(config)) +} const notify: Platform["notify"] = async (title, description, href) => { if (!("Notification" in window)) return @@ -110,9 +127,9 @@ const platform: Platform = { setDefaultServerUrl: writeDefaultServerUrl, } -const defaultUrl = iife(() => { +const defaultUrl: string = iife(() => { const lsDefault = readDefaultServerUrl() - if (lsDefault) return lsDefault + if (lsDefault) return lsDefault.url if (location.hostname.includes("opencode.ai")) return "http://localhost:4096" if (import.meta.env.DEV) return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}` diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts index 6c870dfa4d0..594aeda5e37 100644 --- a/packages/app/src/index.ts +++ b/packages/app/src/index.ts @@ -1,5 +1,5 @@ export { AppBaseProviders, AppInterface } from "./app" export { useCommand } from "./context/command" -export { type DisplayBackend, type Platform, PlatformProvider } from "./context/platform" +export { type DisplayBackend, type Platform, PlatformProvider, type DefaultServerConfig } from "./context/platform" export { ServerConnection } from "./context/server" export { handleNotificationClick } from "./utils/notification-click" diff --git a/packages/desktop-electron/src/main/index.ts b/packages/desktop-electron/src/main/index.ts index 03c1e128ea5..2a6cae5d0e0 100644 --- a/packages/desktop-electron/src/main/index.ts +++ b/packages/desktop-electron/src/main/index.ts @@ -128,9 +128,9 @@ function setInitStep(step: InitStep) { async function setupServerConnection(): Promise { const customUrl = await getSavedServerUrl() - if (customUrl && (await checkHealthOrAskRetry(customUrl))) { - serverReady.resolve({ url: customUrl, password: null }) - return { variant: "existing", url: customUrl } + if (customUrl && (await checkHealthOrAskRetry(customUrl.url, customUrl.username, customUrl.password))) { + serverReady.resolve({ url: customUrl.url, password: null }) + return { variant: "existing", url: customUrl.url } } const port = await getSidecarPort() diff --git a/packages/desktop-electron/src/main/ipc.ts b/packages/desktop-electron/src/main/ipc.ts index bbb5379bb7a..9236617dc9d 100644 --- a/packages/desktop-electron/src/main/ipc.ts +++ b/packages/desktop-electron/src/main/ipc.ts @@ -2,15 +2,21 @@ import { execFile } from "node:child_process" import { BrowserWindow, Notification, app, clipboard, dialog, ipcMain, shell } from "electron" import type { IpcMainEvent, IpcMainInvokeEvent } from "electron" -import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types" +import type { + InitStep, + ServerReadyData, + SqliteMigrationProgress, + WslConfig, + DefaultServerConfig, +} from "../preload/types" import { getStore } from "./store" type Deps = { killSidecar: () => void installCli: () => Promise awaitInitialization: (sendStep: (step: InitStep) => void) => Promise - getDefaultServerUrl: () => Promise | string | null - setDefaultServerUrl: (url: string | null) => Promise | void + getDefaultServerUrl: () => Promise | DefaultServerConfig | null + setDefaultServerUrl: (config: DefaultServerConfig | null) => Promise | void getWslConfig: () => Promise setWslConfig: (config: WslConfig) => Promise | void getDisplayBackend: () => Promise @@ -33,8 +39,8 @@ export function registerIpcHandlers(deps: Deps) { return deps.awaitInitialization(send) }) ipcMain.handle("get-default-server-url", () => deps.getDefaultServerUrl()) - ipcMain.handle("set-default-server-url", (_event: IpcMainInvokeEvent, url: string | null) => - deps.setDefaultServerUrl(url), + ipcMain.handle("set-default-server-url", (_event: IpcMainInvokeEvent, config: DefaultServerConfig | null) => + deps.setDefaultServerUrl(config), ) ipcMain.handle("get-wsl-config", () => deps.getWslConfig()) ipcMain.handle("set-wsl-config", (_event: IpcMainInvokeEvent, config: WslConfig) => deps.setWslConfig(config)) diff --git a/packages/desktop-electron/src/main/server.ts b/packages/desktop-electron/src/main/server.ts index 92018e72e75..1f5a5139f91 100644 --- a/packages/desktop-electron/src/main/server.ts +++ b/packages/desktop-electron/src/main/server.ts @@ -8,14 +8,26 @@ export type WslConfig = { enabled: boolean } export type HealthCheck = { wait: Promise } -export function getDefaultServerUrl(): string | null { +export type DefaultServerConfig = { + url: string + username?: string + password?: string +} + +export function getDefaultServerUrl(): DefaultServerConfig | null { const value = store.get(DEFAULT_SERVER_URL_KEY) - return typeof value === "string" ? value : null + if (typeof value === "string") { + return { url: value } + } + if (value && typeof value === "object" && "url" in value) { + return value as DefaultServerConfig + } + return null } -export function setDefaultServerUrl(url: string | null) { - if (url) { - store.set(DEFAULT_SERVER_URL_KEY, url) +export function setDefaultServerUrl(config: DefaultServerConfig | null) { + if (config) { + store.set(DEFAULT_SERVER_URL_KEY, config) return } @@ -31,13 +43,14 @@ export function setWslConfig(config: WslConfig) { store.set(WSL_ENABLED_KEY, config.enabled) } -export async function getSavedServerUrl(): Promise { +export async function getSavedServerUrl(): Promise { const direct = getDefaultServerUrl() if (direct) return direct const config = await getConfig().catch(() => null) if (!config) return null - return getServerUrlFromConfig(config) + const url = getServerUrlFromConfig(config) + return url ? { url } : null } export function spawnLocalServer(hostname: string, port: number, password: string) { @@ -94,9 +107,10 @@ export async function checkHealth(url: string, password?: string | null): Promis } } -export async function checkHealthOrAskRetry(url: string): Promise { +export async function checkHealthOrAskRetry(url: string, username?: string, password?: string): Promise { while (true) { - if (await checkHealth(url)) return true + const auth = username && password ? Buffer.from(`${username}:${password}`).toString("base64") : password + if (await checkHealth(url, auth)) return true const result = await dialog.showMessageBox({ type: "warning", diff --git a/packages/desktop-electron/src/preload/types.ts b/packages/desktop-electron/src/preload/types.ts index af5410f5f55..fb1fa83e644 100644 --- a/packages/desktop-electron/src/preload/types.ts +++ b/packages/desktop-electron/src/preload/types.ts @@ -9,14 +9,20 @@ export type SqliteMigrationProgress = { type: "InProgress"; value: number } | { export type WslConfig = { enabled: boolean } +export type DefaultServerConfig = { + url: string + username?: string + password?: string +} + export type LinuxDisplayBackend = "wayland" | "auto" export type ElectronAPI = { killSidecar: () => Promise installCli: () => Promise awaitInitialization: (onStep: (step: InitStep) => void) => Promise - getDefaultServerUrl: () => Promise - setDefaultServerUrl: (url: string | null) => Promise + getDefaultServerUrl: () => Promise + setDefaultServerUrl: (config: DefaultServerConfig | null) => Promise getWslConfig: () => Promise setWslConfig: (config: WslConfig) => Promise getDisplayBackend: () => Promise diff --git a/packages/desktop-electron/src/renderer/index.tsx b/packages/desktop-electron/src/renderer/index.tsx index b5193d626bd..d0de7bbc2ee 100644 --- a/packages/desktop-electron/src/renderer/index.tsx +++ b/packages/desktop-electron/src/renderer/index.tsx @@ -202,8 +202,8 @@ const createPlatform = (): Platform => { return window.api.getDefaultServerUrl().catch(() => null) }, - setDefaultServerUrl: async (url: string | null) => { - await window.api.setDefaultServerUrl(url) + setDefaultServerUrl: async (config: import("@opencode-ai/app").DefaultServerConfig | null) => { + await window.api.setDefaultServerUrl(config) }, getDisplayBackend: async () => { diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 9afabe918b1..a1fb96436b9 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -348,13 +348,14 @@ const createPlatform = (): Platform => { await commands.setWslConfig({ enabled }) }, - getDefaultServerUrl: async () => { - const result = await commands.getDefaultServerUrl().catch(() => null) - return result + getDefaultServerUrl: async (): Promise => { + const url = await commands.getDefaultServerUrl().catch(() => null) + if (!url) return null + return { url } }, - setDefaultServerUrl: async (url: string | null) => { - await commands.setDefaultServerUrl(url) + setDefaultServerUrl: async (cfg: import("@opencode-ai/app").DefaultServerConfig | null) => { + await commands.setDefaultServerUrl(cfg?.url ?? null) }, getDisplayBackend: async () => { @@ -413,8 +414,9 @@ render(() => { const platform = createPlatform() const [defaultServer] = createResource(() => - platform.getDefaultServerUrl?.().then((url) => { - if (url) return ServerConnection.key({ type: "http", http: { url } }) + platform.getDefaultServerUrl?.().then((cfg) => { + if (!cfg) return + return ServerConnection.key({ type: "http", http: { url: cfg.url } }) }), )