Skip to content
Merged
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
4 changes: 4 additions & 0 deletions packages/desktop-electron/electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ export default defineConfig({
build: {
rollupOptions: {
input: { index: "src/preload/index.ts" },
output: {
format: "cjs",
entryFileNames: "[name].js",
},
},
},
},
Expand Down
11 changes: 4 additions & 7 deletions packages/desktop-electron/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,15 +195,10 @@ async function initialize() {
logger.log("loading task finished")
})()

const globals = {
updaterEnabled: UPDATER_ENABLED,
deepLinks: pendingDeepLinks,
}

if (needsMigration) {
const show = await Promise.race([loadingTask.then(() => false), delay(1_000).then(() => true)])
if (show) {
overlay = createLoadingWindow(globals)
overlay = createLoadingWindow()
await delay(1_000)
}
}
Expand All @@ -215,7 +210,7 @@ async function initialize() {
await loadingComplete.promise
}

mainWindow = createMainWindow(globals)
mainWindow = createMainWindow()
wireMenu()

overlay?.close()
Expand Down Expand Up @@ -252,6 +247,8 @@ registerIpcHandlers({
initEmitter.off("step", listener)
}
},
getWindowConfig: () => ({ updaterEnabled: UPDATER_ENABLED }),
consumeInitialDeepLinks: () => pendingDeepLinks.splice(0),
getDefaultServerUrl: () => getDefaultServerUrl(),
setDefaultServerUrl: (url) => setDefaultServerUrl(url),
getWslConfig: () => Promise.resolve(getWslConfig()),
Expand Down
13 changes: 12 additions & 1 deletion packages/desktop-electron/src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ 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, TitlebarTheme, WslConfig } from "../preload/types"
import type {
InitStep,
ServerReadyData,
SqliteMigrationProgress,
TitlebarTheme,
WindowConfig,
WslConfig,
} from "../preload/types"
import { getStore } from "./store"
import { setTitlebar } from "./windows"

Expand All @@ -14,6 +21,8 @@ const pickerFilters = (ext?: string[]) => {
type Deps = {
killSidecar: () => void
awaitInitialization: (sendStep: (step: InitStep) => void) => Promise<ServerReadyData>
getWindowConfig: () => Promise<WindowConfig> | WindowConfig
consumeInitialDeepLinks: () => Promise<string[]> | string[]
getDefaultServerUrl: () => Promise<string | null> | string | null
setDefaultServerUrl: (url: string | null) => Promise<void> | void
getWslConfig: () => Promise<WslConfig>
Expand All @@ -37,6 +46,8 @@ export function registerIpcHandlers(deps: Deps) {
const send = (step: InitStep) => event.sender.send("init-step", step)
return deps.awaitInitialization(send)
})
ipcMain.handle("get-window-config", () => deps.getWindowConfig())
ipcMain.handle("consume-initial-deep-links", () => deps.consumeInitialDeepLinks())
ipcMain.handle("get-default-server-url", () => deps.getDefaultServerUrl())
ipcMain.handle("set-default-server-url", (_event: IpcMainInvokeEvent, url: string | null) =>
deps.setDefaultServerUrl(url),
Expand Down
2 changes: 1 addition & 1 deletion packages/desktop-electron/src/main/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function createMenu(deps: Deps) {
{
label: "New Window",
accelerator: "Cmd+Shift+N",
click: () => createMainWindow({ updaterEnabled: UPDATER_ENABLED }),
click: () => createMainWindow(),
},
{ type: "separator" },
{ role: "close" },
Expand Down
37 changes: 10 additions & 27 deletions packages/desktop-electron/src/main/windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@ import { dirname, isAbsolute, join, relative, resolve } from "node:path"
import { fileURLToPath, pathToFileURL } from "node:url"
import type { TitlebarTheme } from "../preload/types"

type Globals = {
updaterEnabled: boolean
deepLinks?: string[]
}

const root = dirname(fileURLToPath(import.meta.url))
const rendererRoot = join(root, "../renderer")
const rendererProtocol = "oc"
Expand Down Expand Up @@ -68,7 +63,7 @@ export function setDockIcon() {
if (!icon.isEmpty()) app.dock?.setIcon(icon)
}

export function createMainWindow(globals: Globals) {
export function createMainWindow() {
const state = windowState({
defaultWidth: 1280,
defaultHeight: 800,
Expand Down Expand Up @@ -98,15 +93,16 @@ export function createMainWindow(globals: Globals) {
}
: {}),
webPreferences: {
preload: join(root, "../preload/index.mjs"),
sandbox: false,
preload: join(root, "../preload/index.js"),
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
})

state.manage(win)
loadWindow(win, "index.html")
wireZoom(win)
injectGlobals(win, globals)

win.once("ready-to-show", () => {
win.show()
Expand All @@ -115,7 +111,7 @@ export function createMainWindow(globals: Globals) {
return win
}

export function createLoadingWindow(globals: Globals) {
export function createLoadingWindow() {
const mode = tone()
const win = new BrowserWindow({
width: 640,
Expand All @@ -134,13 +130,14 @@ export function createLoadingWindow(globals: Globals) {
}
: {}),
webPreferences: {
preload: join(root, "../preload/index.mjs"),
sandbox: false,
preload: join(root, "../preload/index.js"),
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
})

loadWindow(win, "loading.html")
injectGlobals(win, globals)

return win
}
Expand Down Expand Up @@ -174,20 +171,6 @@ function loadWindow(win: BrowserWindow, html: string) {

void win.loadURL(`${rendererProtocol}://${rendererHost}/${html}`)
}

function injectGlobals(win: BrowserWindow, globals: Globals) {
win.webContents.on("dom-ready", () => {
const deepLinks = globals.deepLinks ?? []
const data = {
updaterEnabled: globals.updaterEnabled,
deepLinks: Array.isArray(deepLinks) ? deepLinks.splice(0) : deepLinks,
}
void win.webContents.executeJavaScript(
`window.__OPENCODE__ = Object.assign(window.__OPENCODE__ ?? {}, ${JSON.stringify(data)})`,
)
})
}

function wireZoom(win: BrowserWindow) {
win.webContents.setZoomFactor(1)
win.webContents.on("zoom-changed", () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/desktop-electron/src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const api: ElectronAPI = {
ipcRenderer.removeListener("init-step", handler)
})
},
getWindowConfig: () => ipcRenderer.invoke("get-window-config"),
consumeInitialDeepLinks: () => ipcRenderer.invoke("consume-initial-deep-links"),
getDefaultServerUrl: () => ipcRenderer.invoke("get-default-server-url"),
setDefaultServerUrl: (url) => ipcRenderer.invoke("set-default-server-url", url),
getWslConfig: () => ipcRenderer.invoke("get-wsl-config"),
Expand Down
6 changes: 6 additions & 0 deletions packages/desktop-electron/src/preload/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,16 @@ export type TitlebarTheme = {
mode: "light" | "dark"
}

export type WindowConfig = {
updaterEnabled: boolean
}

export type ElectronAPI = {
killSidecar: () => Promise<void>
installCli: () => Promise<string>
awaitInitialization: (onStep: (step: InitStep) => void) => Promise<ServerReadyData>
getWindowConfig: () => Promise<WindowConfig>
consumeInitialDeepLinks: () => Promise<string[]>
getDefaultServerUrl: () => Promise<string | null>
setDefaultServerUrl: (url: string | null) => Promise<void>
getWslConfig: () => Promise<WslConfig>
Expand Down
2 changes: 0 additions & 2 deletions packages/desktop-electron/src/renderer/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ declare global {
interface Window {
api: ElectronAPI
__OPENCODE__?: {
updaterEnabled?: boolean
wsl?: boolean
deepLinks?: string[]
}
}
Expand Down
30 changes: 16 additions & 14 deletions packages/desktop-electron/src/renderer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { createEffect, createResource, onCleanup, onMount, Show } from "solid-js
import { render } from "solid-js/web"
import pkg from "../../package.json"
import { initI18n, t } from "./i18n"
import { UPDATER_ENABLED } from "./updater"
import { webviewZoom } from "./webview-zoom"
import "./styles.css"
import { useTheme } from "@opencode-ai/ui/theme"
Expand All @@ -43,8 +42,7 @@ const emitDeepLinks = (urls: string[]) => {
}

const listenForDeepLinks = () => {
const startUrls = window.__OPENCODE__?.deepLinks ?? []
if (startUrls.length) emitDeepLinks(startUrls)
void window.api.consumeInitialDeepLinks().then((urls) => emitDeepLinks(urls))
return window.api.onDeepLink((urls) => emitDeepLinks(urls))
}

Expand All @@ -57,13 +55,18 @@ const createPlatform = (): Platform => {
return undefined
})()

const isWslEnabled = async () => {
if (os !== "windows") return false
return window.api.getWslConfig().then((config) => config.enabled).catch(() => false)
}

const wslHome = async () => {
if (os !== "windows" || !window.__OPENCODE__?.wsl) return undefined
if (!(await isWslEnabled())) return undefined
return window.api.wslPath("~", "windows").catch(() => undefined)
}

const handleWslPicker = async <T extends string | string[]>(result: T | null): Promise<T | null> => {
if (!result || !window.__OPENCODE__?.wsl) return result
if (!result || !(await isWslEnabled())) return result
if (Array.isArray(result)) {
return Promise.all(result.map((path) => window.api.wslPath(path, "linux").catch(() => path))) as any
}
Expand Down Expand Up @@ -137,7 +140,7 @@ const createPlatform = (): Platform => {
if (os === "windows") {
const resolvedApp = app ? await window.api.resolveAppPath(app).catch(() => null) : null
const resolvedPath = await (async () => {
if (window.__OPENCODE__?.wsl) {
if (await isWslEnabled()) {
const converted = await window.api.wslPath(path, "windows").catch(() => null)
if (converted) return converted
}
Expand All @@ -159,12 +162,14 @@ const createPlatform = (): Platform => {
storage,

checkUpdate: async () => {
if (!UPDATER_ENABLED()) return { updateAvailable: false }
const config = await window.api.getWindowConfig().catch(() => ({ updaterEnabled: false }))
if (!config.updaterEnabled) return { updateAvailable: false }
return window.api.checkUpdate()
},

update: async () => {
if (!UPDATER_ENABLED()) return
const config = await window.api.getWindowConfig().catch(() => ({ updaterEnabled: false }))
if (!config.updaterEnabled) return
await window.api.installUpdate()
},

Expand Down Expand Up @@ -194,11 +199,7 @@ const createPlatform = (): Platform => {
return fetch(input, init)
},

getWslEnabled: async () => {
const next = await window.api.getWslConfig().catch(() => null)
if (next) return next.enabled
return window.__OPENCODE__!.wsl ?? false
},
getWslEnabled: () => isWslEnabled(),

setWslEnabled: async (enabled) => {
await window.api.setWslConfig({ enabled })
Expand Down Expand Up @@ -249,6 +250,7 @@ listenForDeepLinks()

render(() => {
const platform = createPlatform()
const [windowConfig] = createResource(() => window.api.getWindowConfig().catch(() => ({ updaterEnabled: false })))
const loadLocale = async () => {
const current = await platform.storage?.("opencode.global.dat").getItem("language")
const legacy = current ? undefined : await platform.storage?.().getItem("language.v1")
Expand Down Expand Up @@ -325,7 +327,7 @@ render(() => {
return (
<PlatformProvider value={platform}>
<AppBaseProviders locale={locale.latest}>
<Show when={!defaultServer.loading && !sidecar.loading && !windowCount.loading && !locale.loading}>
<Show when={!defaultServer.loading && !sidecar.loading && !windowConfig.loading && !windowCount.loading && !locale.loading}>
{(_) => {
return (
<AppInterface
Expand Down
2 changes: 0 additions & 2 deletions packages/desktop-electron/src/renderer/updater.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { initI18n, t } from "./i18n"

export const UPDATER_ENABLED = () => window.__OPENCODE__?.updaterEnabled ?? false

export async function runUpdater({ alertOnFail }: { alertOnFail: boolean }) {
await initI18n()
try {
Expand Down
Loading