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
29 changes: 21 additions & 8 deletions packages/app/src/components/dialog-select-server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ function useDefaultServer(platform: ReturnType<typeof usePlatform>, 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
Expand All @@ -57,10 +57,17 @@ function useDefaultServer(platform: ReturnType<typeof usePlatform>, 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)
}
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -585,7 +593,12 @@ export function DialogSelectServer() {
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<Show when={canDefault() && defaultUrl() !== i.http.url}>
<DropdownMenu.Item onSelect={() => setDefault(i.http.url)}>
<DropdownMenu.Item
onSelect={() => {
if (i.type !== "http") return
setDefault(i)
}}
>
<DropdownMenu.ItemLabel>
{language.t("dialog.server.menu.default")}
</DropdownMenu.ItemLabel>
Expand Down
12 changes: 9 additions & 3 deletions packages/app/src/components/status-popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,13 @@ const useServerHealth = (servers: Accessor<ServerConnection.Any[]>, fetcher: typ
}

const useDefaultServerKey = (
get: (() => string | Promise<string | null | undefined> | null | undefined) | undefined,
get:
| (() =>
| import("@/context/platform").DefaultServerConfig
| Promise<import("@/context/platform").DefaultServerConfig | null | undefined>
| null
| undefined)
| undefined,
) => {
const [url, setUrl] = createSignal<string | undefined>()
const [tick, setTick] = createSignal(0)
Expand All @@ -103,15 +109,15 @@ 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
})
return
}

setUrl(normalizeServerUrl(result))
setUrl(normalizeServerUrl(result.url))
onCleanup(() => {
dead = true
})
Expand Down
10 changes: 8 additions & 2 deletions packages/app/src/context/platform.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -58,10 +64,10 @@ export type Platform = {
fetch?: typeof fetch

/** Get the configured default server URL (platform-specific) */
getDefaultServerUrl?(): Promise<string | null>
getDefaultServerUrl?(): Promise<DefaultServerConfig | null>

/** Set the default server URL to use on app startup (platform-specific) */
setDefaultServerUrl?(url: string | null): Promise<void> | void
setDefaultServerUrl?(config: DefaultServerConfig | null): Promise<void> | void

/** Get the configured WSL integration (desktop only) */
getWslEnabled?(): Promise<boolean>
Expand Down
25 changes: 21 additions & 4 deletions packages/app/src/entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"}`
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/index.ts
Original file line number Diff line number Diff line change
@@ -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"
6 changes: 3 additions & 3 deletions packages/desktop-electron/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,9 @@ function setInitStep(step: InitStep) {
async function setupServerConnection(): Promise<ServerConnection> {
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()
Expand Down
16 changes: 11 additions & 5 deletions packages/desktop-electron/src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>
awaitInitialization: (sendStep: (step: InitStep) => void) => Promise<ServerReadyData>
getDefaultServerUrl: () => Promise<string | null> | string | null
setDefaultServerUrl: (url: string | null) => Promise<void> | void
getDefaultServerUrl: () => Promise<DefaultServerConfig | null> | DefaultServerConfig | null
setDefaultServerUrl: (config: DefaultServerConfig | null) => Promise<void> | void
getWslConfig: () => Promise<WslConfig>
setWslConfig: (config: WslConfig) => Promise<void> | void
getDisplayBackend: () => Promise<string | null>
Expand All @@ -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))
Expand Down
32 changes: 23 additions & 9 deletions packages/desktop-electron/src/main/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,26 @@ export type WslConfig = { enabled: boolean }

export type HealthCheck = { wait: Promise<void> }

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
}

Expand All @@ -31,13 +43,14 @@ export function setWslConfig(config: WslConfig) {
store.set(WSL_ENABLED_KEY, config.enabled)
}

export async function getSavedServerUrl(): Promise<string | null> {
export async function getSavedServerUrl(): Promise<DefaultServerConfig | null> {
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) {
Expand Down Expand Up @@ -94,9 +107,10 @@ export async function checkHealth(url: string, password?: string | null): Promis
}
}

export async function checkHealthOrAskRetry(url: string): Promise<boolean> {
export async function checkHealthOrAskRetry(url: string, username?: string, password?: string): Promise<boolean> {
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",
Expand Down
10 changes: 8 additions & 2 deletions packages/desktop-electron/src/preload/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>
installCli: () => Promise<string>
awaitInitialization: (onStep: (step: InitStep) => void) => Promise<ServerReadyData>
getDefaultServerUrl: () => Promise<string | null>
setDefaultServerUrl: (url: string | null) => Promise<void>
getDefaultServerUrl: () => Promise<DefaultServerConfig | null>
setDefaultServerUrl: (config: DefaultServerConfig | null) => Promise<void>
getWslConfig: () => Promise<WslConfig>
setWslConfig: (config: WslConfig) => Promise<void>
getDisplayBackend: () => Promise<LinuxDisplayBackend | null>
Expand Down
4 changes: 2 additions & 2 deletions packages/desktop-electron/src/renderer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
16 changes: 9 additions & 7 deletions packages/desktop/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<import("@opencode-ai/app").DefaultServerConfig | null> => {
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 () => {
Expand Down Expand Up @@ -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 } })
}),
)

Expand Down
Loading