refactor(desktop): establish typed ipc contract (#43150)
This commit is contained in:
@@ -10,10 +10,16 @@ import { app, BrowserWindow } from "electron"
|
||||
import { Deferred, Effect, Fiber } from "effect"
|
||||
import contextMenu from "electron-context-menu"
|
||||
|
||||
import type { ServerReadyData } from "../preload/types"
|
||||
import type { ServerReadyData } from "../shared/ipc-contract"
|
||||
import { checkAppExists, resolveAppPath } from "./apps"
|
||||
import { CHANNEL, VERSION } from "./constants"
|
||||
import { registerIpcHandlers, sendDeepLinks, sendMenuCommand } from "./ipc"
|
||||
import {
|
||||
registerIpcHandlers,
|
||||
registerUpdaterIpcHandlers,
|
||||
registerWslIpcHandlers,
|
||||
sendDeepLinks,
|
||||
sendMenuCommand,
|
||||
} from "./ipc"
|
||||
import { forwardInitializationFailure } from "./initialization"
|
||||
import { exportDebugLogs, initCrashReporter, initLogging, startNetLog, write as writeLog } from "./logging"
|
||||
import { createMenu } from "./menu"
|
||||
@@ -23,7 +29,7 @@ import {
|
||||
isFirstLaunchOnboardingPending,
|
||||
} from "./onboarding"
|
||||
import { getDefaultServerUrl, preferAppEnv, setDefaultServerUrl } from "./server"
|
||||
import { registerUpdaterIpc, setupAutoUpdater, showUpdaterDialog } from "./updater"
|
||||
import { createUpdaterIpc, setupAutoUpdater, showUpdaterDialog } from "./updater"
|
||||
import { safeWebContentsURL } from "./window-state"
|
||||
import {
|
||||
getLastFocusedWindow,
|
||||
@@ -34,7 +40,7 @@ import {
|
||||
setDockIcon,
|
||||
restoreMainWindows,
|
||||
} from "./windows"
|
||||
import { registerWslIpcHandlers } from "./wsl/ipc"
|
||||
import { createWslIpc } from "./wsl/ipc"
|
||||
import { cleanupStoreFiles } from "./store-cleanup"
|
||||
import { startBackgroundCli } from "./background-cli"
|
||||
import { setNativeTranslations } from "./native-translations"
|
||||
@@ -267,7 +273,7 @@ const main = Effect.gen(function* () {
|
||||
if (setNativeTranslations(bundle)) createMenu(menuDeps)
|
||||
},
|
||||
})
|
||||
registerUpdaterIpc(updater)
|
||||
registerUpdaterIpcHandlers(createUpdaterIpc(updater))
|
||||
void updater.start()
|
||||
const updateTimer = setInterval(() => void updater.check(), 10 * 60 * 1000)
|
||||
updateTimer.unref()
|
||||
@@ -314,7 +320,7 @@ const main = Effect.gen(function* () {
|
||||
|
||||
async function startWslServers(cli: { version: string; wslBuild?: { script: string; output: string } }) {
|
||||
if (process.platform !== "win32") {
|
||||
registerWslIpcHandlers()
|
||||
registerWslIpcHandlers(createWslIpc())
|
||||
return async () => {}
|
||||
}
|
||||
|
||||
@@ -344,7 +350,7 @@ async function startWslServers(cli: { version: string; wslBuild?: { script: stri
|
||||
error: (message, meta) => logger.error(message, meta),
|
||||
},
|
||||
})
|
||||
registerWslIpcHandlers(controller)
|
||||
registerWslIpcHandlers(createWslIpc(controller))
|
||||
controller.startConfiguredServers()
|
||||
return async () => controller.stopServers()
|
||||
}
|
||||
|
||||
+134
-103
@@ -3,10 +3,18 @@ import { stat } from "node:fs/promises"
|
||||
import { basename, join } from "node:path"
|
||||
import { app, BrowserWindow, clipboard, dialog, ipcMain, shell } from "electron"
|
||||
import type { IpcMainEvent, IpcMainInvokeEvent } from "electron"
|
||||
import type { DesktopMenuAction } from "@opencode-ai/app/desktop-menu"
|
||||
import { parseDesktopNativeBundle, type DesktopNativeBundle } from "@opencode-ai/app/i18n/desktop-native"
|
||||
|
||||
import type { FatalRendererError, ServerReadyData, TitlebarTheme } from "../preload/types"
|
||||
import {
|
||||
Ipc,
|
||||
sendIpcEvent,
|
||||
type FatalRendererError,
|
||||
type IpcInvoke,
|
||||
type IpcInvokeArgs,
|
||||
type IpcInvokeResult,
|
||||
type IpcSend,
|
||||
type ServerReadyData,
|
||||
} from "../shared/ipc-contract"
|
||||
import { runDesktopMenuAction } from "./desktop-menu-actions"
|
||||
import { setForceFocus } from "./debug"
|
||||
import { assertAttachmentBudget, createPickedFileAuthorizations } from "./attachment-picker"
|
||||
@@ -22,6 +30,24 @@ import {
|
||||
} from "./windows"
|
||||
import { createDesktopDraftStore } from "./draft-store"
|
||||
import { nativeT } from "./native-translations"
|
||||
import type { UpdaterIpc } from "./updater"
|
||||
import type { WslIpc } from "./wsl/ipc"
|
||||
|
||||
type MaybePromise<Value> = Value | Promise<Value>
|
||||
|
||||
function handle<Channel extends keyof IpcInvoke>(
|
||||
channel: Channel,
|
||||
listener: (event: IpcMainInvokeEvent, ...args: IpcInvokeArgs<Channel>) => MaybePromise<IpcInvokeResult<Channel>>,
|
||||
) {
|
||||
ipcMain.handle(channel, listener)
|
||||
}
|
||||
|
||||
function on<Channel extends keyof IpcSend>(
|
||||
channel: Channel,
|
||||
listener: (event: IpcMainEvent, ...args: IpcSend[Channel]) => void,
|
||||
) {
|
||||
ipcMain.on(channel, listener)
|
||||
}
|
||||
|
||||
const pickerFilters = (ext?: string[]) => {
|
||||
if (!ext || ext.length === 0) return undefined
|
||||
@@ -53,27 +79,21 @@ export function registerIpcHandlers(deps: Deps) {
|
||||
app.once("will-quit", () => drafts.close())
|
||||
app.on("browser-window-created", (_event, win) => win.on("session-end", () => drafts.flush()))
|
||||
|
||||
ipcMain.handle("await-initialization", () => deps.awaitInitialization())
|
||||
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),
|
||||
)
|
||||
ipcMain.handle("is-first-launch-onboarding-pending", () => deps.isFirstLaunchOnboardingPending())
|
||||
ipcMain.handle("finish-first-launch-onboarding", (_event: IpcMainInvokeEvent, createDefaultProject: boolean) =>
|
||||
handle(Ipc.app.awaitInitialization, () => deps.awaitInitialization())
|
||||
handle(Ipc.app.consumeInitialDeepLinks, () => deps.consumeInitialDeepLinks())
|
||||
handle(Ipc.app.getDefaultServerUrl, () => deps.getDefaultServerUrl())
|
||||
handle(Ipc.app.setDefaultServerUrl, (_event, url) => deps.setDefaultServerUrl(url))
|
||||
handle(Ipc.app.isFirstLaunchOnboardingPending, () => deps.isFirstLaunchOnboardingPending())
|
||||
handle(Ipc.app.finishFirstLaunchOnboarding, (_event, createDefaultProject) =>
|
||||
deps.finishFirstLaunchOnboarding(createDefaultProject),
|
||||
)
|
||||
ipcMain.handle("check-app-exists", (_event: IpcMainInvokeEvent, appName: string) => deps.checkAppExists(appName))
|
||||
ipcMain.handle("resolve-app-path", (_event: IpcMainInvokeEvent, appName: string) => deps.resolveAppPath(appName))
|
||||
ipcMain.handle("set-background-color", (_event: IpcMainInvokeEvent, color: string) => deps.setBackgroundColor(color))
|
||||
ipcMain.handle("export-debug-logs", () => deps.exportDebugLogs())
|
||||
ipcMain.handle("set-force-focus", (event: IpcMainInvokeEvent, enabled: boolean) =>
|
||||
setForceFocus(event.sender, enabled),
|
||||
)
|
||||
ipcMain.handle("record-fatal-renderer-error", (_event: IpcMainInvokeEvent, error: FatalRendererError) =>
|
||||
deps.recordFatalRendererError(error),
|
||||
)
|
||||
ipcMain.handle("set-native-translations", (event: IpcMainInvokeEvent, value: unknown) => {
|
||||
handle(Ipc.app.checkAppExists, (_event, appName) => deps.checkAppExists(appName))
|
||||
handle(Ipc.app.resolveAppPath, (_event, appName) => deps.resolveAppPath(appName))
|
||||
handle(Ipc.app.setBackgroundColor, (_event, color) => deps.setBackgroundColor(color))
|
||||
handle(Ipc.app.exportDebugLogs, () => deps.exportDebugLogs())
|
||||
handle(Ipc.app.setForceFocus, (event, enabled) => setForceFocus(event.sender, enabled))
|
||||
handle(Ipc.app.recordFatalRendererError, (_event, error) => deps.recordFatalRendererError(error))
|
||||
handle(Ipc.app.setNativeTranslations, (event, value) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
if (!win || win.isDestroyed() || win.webContents !== event.sender || event.senderFrame !== event.sender.mainFrame) {
|
||||
throw new Error("Invalid native translation sender")
|
||||
@@ -82,7 +102,7 @@ export function registerIpcHandlers(deps: Deps) {
|
||||
if (!bundle) throw new Error("Invalid native translation bundle")
|
||||
deps.setNativeTranslations(bundle)
|
||||
})
|
||||
ipcMain.handle("store-get", (_event: IpcMainInvokeEvent, name: string, key: string) => {
|
||||
handle(Ipc.storage.get, (_event, name, key) => {
|
||||
try {
|
||||
const store = getStore(name)
|
||||
const value = store.get(key)
|
||||
@@ -92,102 +112,90 @@ export function registerIpcHandlers(deps: Deps) {
|
||||
return null
|
||||
}
|
||||
})
|
||||
ipcMain.handle("store-set", (_event: IpcMainInvokeEvent, name: string, key: string, value: string) => {
|
||||
handle(Ipc.storage.set, (_event, name, key, value) => {
|
||||
getStore(name).set(key, value)
|
||||
})
|
||||
ipcMain.handle("store-delete", (_event: IpcMainInvokeEvent, name: string, key: string) => {
|
||||
handle(Ipc.storage.delete, (_event, name, key) => {
|
||||
getStore(name).delete(key)
|
||||
void removeStoreFileIfEmpty(name)
|
||||
})
|
||||
ipcMain.handle("store-clear", (_event: IpcMainInvokeEvent, name: string) => {
|
||||
handle(Ipc.storage.clear, (_event, name) => {
|
||||
getStore(name).clear()
|
||||
void removeStoreFileIfEmpty(name)
|
||||
})
|
||||
ipcMain.handle("store-keys", (_event: IpcMainInvokeEvent, name: string) => {
|
||||
handle(Ipc.storage.keys, (_event, name) => {
|
||||
const store = getStore(name)
|
||||
return Object.keys(store.store)
|
||||
})
|
||||
ipcMain.handle("store-length", (_event: IpcMainInvokeEvent, name: string) => {
|
||||
handle(Ipc.storage.length, (_event, name) => {
|
||||
const store = getStore(name)
|
||||
return Object.keys(store.store).length
|
||||
})
|
||||
ipcMain.handle("draft-get", (_event, key: string) => drafts.get(key))
|
||||
ipcMain.handle("draft-set", (_event, key: string, value: string) => drafts.set(key, value))
|
||||
ipcMain.handle("draft-delete", (_event, key: string) => drafts.set(key, null))
|
||||
ipcMain.handle("draft-blob-put", (_event, data: ArrayBuffer) => drafts.putBlob(new Uint8Array(data)))
|
||||
ipcMain.handle("draft-blob-get", (_event, id: string) => {
|
||||
handle(Ipc.drafts.get, (_event, key) => drafts.get(key))
|
||||
handle(Ipc.drafts.set, (_event, key, value) => drafts.set(key, value))
|
||||
handle(Ipc.drafts.delete, (_event, key) => drafts.set(key, null))
|
||||
handle(Ipc.drafts.putBlob, (_event, data) => drafts.putBlob(new Uint8Array(data)))
|
||||
handle(Ipc.drafts.getBlob, (_event, id) => {
|
||||
const data = drafts.getBlob(id)
|
||||
return data ? data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) : null
|
||||
return data ? new Uint8Array(data).buffer : null
|
||||
})
|
||||
|
||||
ipcMain.handle(
|
||||
"open-directory-picker",
|
||||
async (_event: IpcMainInvokeEvent, opts?: { multiple?: boolean; title?: string; defaultPath?: string }) => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ["openDirectory", ...(opts?.multiple ? ["multiSelections" as const] : []), "createDirectory"],
|
||||
title: opts?.title ?? nativeT("desktop.dialog.chooseFolder"),
|
||||
defaultPath: opts?.defaultPath,
|
||||
})
|
||||
if (result.canceled) return null
|
||||
return opts?.multiple ? result.filePaths : result.filePaths[0]
|
||||
},
|
||||
)
|
||||
handle(Ipc.files.openDirectoryPicker, async (_event, opts) => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ["openDirectory", ...(opts?.multiple ? ["multiSelections" as const] : []), "createDirectory"],
|
||||
title: opts?.title ?? nativeT("desktop.dialog.chooseFolder"),
|
||||
defaultPath: opts?.defaultPath,
|
||||
})
|
||||
if (result.canceled) return null
|
||||
return opts?.multiple ? result.filePaths : result.filePaths[0]
|
||||
})
|
||||
|
||||
ipcMain.handle(
|
||||
"open-file-picker",
|
||||
async (
|
||||
event: IpcMainInvokeEvent,
|
||||
opts?: { multiple?: boolean; title?: string; defaultPath?: string; extensions?: string[] },
|
||||
) => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ["openFile", ...(opts?.multiple ? ["multiSelections" as const] : [])],
|
||||
title: opts?.title ?? nativeT("desktop.dialog.chooseFile"),
|
||||
defaultPath: opts?.defaultPath,
|
||||
filters: pickerFilters(opts?.extensions),
|
||||
})
|
||||
if (result.canceled) return null
|
||||
const files = await Promise.all(
|
||||
result.filePaths.map(async (filePath) => ({
|
||||
path: filePath,
|
||||
name: basename(filePath),
|
||||
size: (await stat(filePath)).size,
|
||||
})),
|
||||
)
|
||||
assertAttachmentBudget(files)
|
||||
const token = pickedFiles.add(event.sender.id, result.filePaths)
|
||||
return { token, files }
|
||||
},
|
||||
)
|
||||
handle(Ipc.files.openFilePicker, async (event, opts) => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ["openFile", ...(opts?.multiple ? ["multiSelections" as const] : [])],
|
||||
title: opts?.title ?? nativeT("desktop.dialog.chooseFile"),
|
||||
defaultPath: opts?.defaultPath,
|
||||
filters: pickerFilters(opts?.extensions),
|
||||
})
|
||||
if (result.canceled) return null
|
||||
const files = await Promise.all(
|
||||
result.filePaths.map(async (filePath) => ({
|
||||
path: filePath,
|
||||
name: basename(filePath),
|
||||
size: (await stat(filePath)).size,
|
||||
})),
|
||||
)
|
||||
assertAttachmentBudget(files)
|
||||
const token = pickedFiles.add(event.sender.id, result.filePaths)
|
||||
return { token, files }
|
||||
})
|
||||
|
||||
ipcMain.handle("read-picked-file", async (event: IpcMainInvokeEvent, token: string, filePath: string) => {
|
||||
handle(Ipc.files.readPickedFile, async (event, token, filePath) => {
|
||||
return pickedFiles.read(event.sender.id, token, filePath)
|
||||
})
|
||||
|
||||
ipcMain.handle("release-picked-files", (event: IpcMainInvokeEvent, token: string) => {
|
||||
handle(Ipc.files.releasePickedFiles, (event, token) => {
|
||||
pickedFiles.release(event.sender.id, token)
|
||||
})
|
||||
|
||||
ipcMain.handle(
|
||||
"save-file-picker",
|
||||
async (_event: IpcMainInvokeEvent, opts?: { title?: string; defaultPath?: string }) => {
|
||||
const result = await dialog.showSaveDialog({
|
||||
title: opts?.title ?? nativeT("desktop.dialog.saveFile"),
|
||||
defaultPath: opts?.defaultPath,
|
||||
})
|
||||
if (result.canceled) return null
|
||||
return result.filePath ?? null
|
||||
},
|
||||
)
|
||||
handle(Ipc.files.saveFilePicker, async (_event, opts) => {
|
||||
const result = await dialog.showSaveDialog({
|
||||
title: opts?.title ?? nativeT("desktop.dialog.saveFile"),
|
||||
defaultPath: opts?.defaultPath,
|
||||
})
|
||||
if (result.canceled) return null
|
||||
return result.filePath ?? null
|
||||
})
|
||||
|
||||
ipcMain.on("open-external", (_event: IpcMainEvent, url: string) => {
|
||||
on(Ipc.files.openExternal, (_event, url) => {
|
||||
openExternalURL(url)
|
||||
})
|
||||
|
||||
ipcMain.on("open-local-file", (_event: IpcMainEvent, url: string) => {
|
||||
on(Ipc.files.openLocalFile, (_event, url) => {
|
||||
openLocalFileURL(url)
|
||||
})
|
||||
|
||||
ipcMain.handle("open-path", async (_event: IpcMainInvokeEvent, path: string, app?: string) => {
|
||||
handle(Ipc.files.openPath, async (_event, path, app) => {
|
||||
if (!app) return shell.openPath(path)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const [cmd, args] =
|
||||
@@ -196,7 +204,7 @@ export function registerIpcHandlers(deps: Deps) {
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.handle("reveal-path", async (_event: IpcMainInvokeEvent, path: string) => {
|
||||
handle(Ipc.files.revealPath, async (_event, path) => {
|
||||
const exists = await stat(path).then(
|
||||
() => true,
|
||||
() => false,
|
||||
@@ -206,15 +214,15 @@ export function registerIpcHandlers(deps: Deps) {
|
||||
return true
|
||||
})
|
||||
|
||||
ipcMain.handle("read-clipboard-image", () => {
|
||||
handle(Ipc.files.readClipboardImage, () => {
|
||||
const image = clipboard.readImage()
|
||||
if (image.isEmpty()) return null
|
||||
const buffer = image.toPNG().buffer
|
||||
const buffer = new Uint8Array(image.toPNG()).buffer
|
||||
const size = image.getSize()
|
||||
return { buffer, width: size.width, height: size.height }
|
||||
})
|
||||
|
||||
ipcMain.handle("get-window-id", (event: IpcMainInvokeEvent) => {
|
||||
handle(Ipc.window.getId, (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
if (!win) throw new Error("Window not found")
|
||||
const id = getWindowID(win)
|
||||
@@ -222,47 +230,47 @@ export function registerIpcHandlers(deps: Deps) {
|
||||
return id
|
||||
})
|
||||
|
||||
ipcMain.handle("get-window-focused", (event: IpcMainInvokeEvent) => {
|
||||
handle(Ipc.window.getFocused, (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
return win?.isFocused() ?? false
|
||||
})
|
||||
|
||||
ipcMain.handle("get-window-fullscreen", (event: IpcMainInvokeEvent) => {
|
||||
handle(Ipc.window.getFullscreen, (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
return win?.isFullScreen() ?? false
|
||||
})
|
||||
|
||||
ipcMain.handle("set-window-focus", (event: IpcMainInvokeEvent) => {
|
||||
handle(Ipc.window.setFocus, (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
win?.focus()
|
||||
})
|
||||
|
||||
ipcMain.handle("show-window", (event: IpcMainInvokeEvent) => {
|
||||
handle(Ipc.window.show, (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
win?.show()
|
||||
})
|
||||
|
||||
ipcMain.on("relaunch", () => {
|
||||
on(Ipc.app.relaunch, () => {
|
||||
deps.relaunch()
|
||||
})
|
||||
|
||||
ipcMain.handle("get-zoom-factor", (event: IpcMainInvokeEvent) => event.sender.getZoomFactor())
|
||||
ipcMain.handle("set-zoom-factor", (event: IpcMainInvokeEvent, factor: number) => {
|
||||
handle(Ipc.window.getZoomFactor, (event) => event.sender.getZoomFactor())
|
||||
handle(Ipc.window.setZoomFactor, (event, factor) => {
|
||||
event.sender.setZoomFactor(factor)
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
if (!win) return
|
||||
updateTitlebar(win)
|
||||
})
|
||||
ipcMain.handle("get-pinch-zoom-enabled", () => getPinchZoomEnabled())
|
||||
ipcMain.handle("set-pinch-zoom-enabled", (_event: IpcMainInvokeEvent, enabled: boolean) => {
|
||||
handle(Ipc.window.getPinchZoomEnabled, () => getPinchZoomEnabled())
|
||||
handle(Ipc.window.setPinchZoomEnabled, (_event, enabled) => {
|
||||
setPinchZoomEnabled(enabled)
|
||||
})
|
||||
ipcMain.handle("set-titlebar", (event: IpcMainInvokeEvent, theme: TitlebarTheme) => {
|
||||
handle(Ipc.window.setTitlebar, (event, theme) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
if (!win) return
|
||||
setTitlebar(win, theme)
|
||||
})
|
||||
ipcMain.handle("run-desktop-menu-action", (event: IpcMainInvokeEvent, action: DesktopMenuAction) => {
|
||||
handle(Ipc.menu.runAction, (event, action) => {
|
||||
runDesktopMenuAction(BrowserWindow.fromWebContents(event.sender), action, {
|
||||
checkForUpdates: () => void deps.showUpdater(),
|
||||
relaunch: deps.relaunch,
|
||||
@@ -270,10 +278,33 @@ export function registerIpcHandlers(deps: Deps) {
|
||||
})
|
||||
}
|
||||
|
||||
export function registerUpdaterIpcHandlers(updater: UpdaterIpc) {
|
||||
handle(Ipc.updater.subscribe, (event) => updater.subscribe(event.sender))
|
||||
handle(Ipc.updater.unsubscribe, (event) => updater.unsubscribe(event.sender.id))
|
||||
handle(Ipc.updater.check, () => updater.check())
|
||||
handle(Ipc.updater.install, () => updater.install())
|
||||
}
|
||||
|
||||
export function registerWslIpcHandlers(wsl: WslIpc) {
|
||||
handle(Ipc.wsl.subscribe, (event) => wsl.subscribe(event.sender))
|
||||
handle(Ipc.wsl.unsubscribe, (event) => wsl.unsubscribe(event.sender.id))
|
||||
handle(Ipc.wsl.getState, () => wsl.getState())
|
||||
handle(Ipc.wsl.probeRuntime, () => wsl.probeRuntime())
|
||||
handle(Ipc.wsl.refreshDistros, () => wsl.refreshDistros())
|
||||
handle(Ipc.wsl.installWsl, () => wsl.installWsl())
|
||||
handle(Ipc.wsl.installDistro, (_event, value) => wsl.installDistro(value))
|
||||
handle(Ipc.wsl.probeAddable, (_event, value) => wsl.probeAddable(value))
|
||||
handle(Ipc.wsl.installOpencode, (_event, value) => wsl.installOpencode(value))
|
||||
handle(Ipc.wsl.openTerminal, (_event, value) => wsl.openTerminal(value))
|
||||
handle(Ipc.wsl.addServer, (_event, value) => wsl.addServer(value))
|
||||
handle(Ipc.wsl.removeServer, (_event, value) => wsl.removeServer(value))
|
||||
handle(Ipc.wsl.startServer, (_event, value) => wsl.startServer(value))
|
||||
}
|
||||
|
||||
export function sendMenuCommand(win: BrowserWindow, id: string) {
|
||||
win.webContents.send("menu-command", id)
|
||||
sendIpcEvent(win.webContents, Ipc.menu.command, id)
|
||||
}
|
||||
|
||||
export function sendDeepLinks(win: BrowserWindow, urls: string[]) {
|
||||
win.webContents.send("deep-link", urls)
|
||||
sendIpcEvent(win.webContents, Ipc.app.deepLink, urls)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { app, dialog, ipcMain } from "electron"
|
||||
import { app, dialog } from "electron"
|
||||
import type { WebContents } from "electron"
|
||||
import { Ipc, sendIpcEvent } from "../shared/ipc-contract"
|
||||
import { UPDATER_ENABLED } from "./constants"
|
||||
import { createUpdaterController, type UpdaterController, type UpdaterReadyRecord } from "./updater-controller"
|
||||
import { getLogger } from "./logging"
|
||||
@@ -28,7 +30,7 @@ export function setupAutoUpdater(prepareToRestart: () => Promise<void>) {
|
||||
})
|
||||
}
|
||||
|
||||
export function registerUpdaterIpc(controller: UpdaterController) {
|
||||
export function createUpdaterIpc(controller: UpdaterController) {
|
||||
const subscriptions = new Map<number, () => void>()
|
||||
const unsubscribe = (id: number) => {
|
||||
subscriptions.get(id)?.()
|
||||
@@ -36,23 +38,27 @@ export function registerUpdaterIpc(controller: UpdaterController) {
|
||||
}
|
||||
app.once("will-quit", () => subscriptions.forEach((dispose) => dispose()))
|
||||
|
||||
ipcMain.handle("updater-subscribe", (event) => {
|
||||
const id = event.sender.id
|
||||
subscriptions.get(id)?.() // a reloaded renderer replaces its previous subscription
|
||||
subscriptions.set(
|
||||
id,
|
||||
controller.subscribe((state) => {
|
||||
if (event.sender.isDestroyed()) return unsubscribe(id)
|
||||
event.sender.send("updater-state", state)
|
||||
}),
|
||||
)
|
||||
event.sender.once("destroyed", () => unsubscribe(id))
|
||||
})
|
||||
ipcMain.handle("updater-unsubscribe", (event) => unsubscribe(event.sender.id))
|
||||
ipcMain.handle("updater-check", () => controller.check())
|
||||
ipcMain.handle("updater-install", () => controller.install())
|
||||
return {
|
||||
subscribe(sender: WebContents) {
|
||||
const id = sender.id
|
||||
subscriptions.get(id)?.() // a reloaded renderer replaces its previous subscription
|
||||
subscriptions.set(
|
||||
id,
|
||||
controller.subscribe((state) => {
|
||||
if (sender.isDestroyed()) return unsubscribe(id)
|
||||
sendIpcEvent(sender, Ipc.updater.state, state)
|
||||
}),
|
||||
)
|
||||
sender.once("destroyed", () => unsubscribe(id))
|
||||
},
|
||||
unsubscribe,
|
||||
check: () => controller.check(),
|
||||
install: () => controller.install(),
|
||||
}
|
||||
}
|
||||
|
||||
export type UpdaterIpc = ReturnType<typeof createUpdaterIpc>
|
||||
|
||||
export async function showUpdaterDialog(controller: UpdaterController) {
|
||||
const state = await controller.check()
|
||||
if (state.status === "error") {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { rmSync } from "node:fs"
|
||||
import { app, BrowserWindow, dialog, net, nativeImage, nativeTheme, protocol, shell } from "electron"
|
||||
import { dirname, isAbsolute, join, relative, resolve } from "node:path"
|
||||
import { fileURLToPath, pathToFileURL } from "node:url"
|
||||
import type { TitlebarTheme } from "../preload/types"
|
||||
import { Ipc, sendIpcEvent, type TitlebarTheme } from "../shared/ipc-contract"
|
||||
import { exportDebugLogs, write as writeLog } from "./logging"
|
||||
import { getStore, removeStoreFile } from "./store"
|
||||
import { PINCH_ZOOM_ENABLED_KEY, WINDOW_IDS_KEY } from "./store-keys"
|
||||
@@ -132,7 +132,7 @@ export function setPinchZoomEnabled(enabled: boolean) {
|
||||
getStore().set(PINCH_ZOOM_ENABLED_KEY, enabled)
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
pinchZoomEnabled.set(win, enabled)
|
||||
win.webContents.send("pinch-zoom-enabled-changed", enabled)
|
||||
sendIpcEvent(win.webContents, Ipc.window.pinchZoomEnabledChanged, enabled)
|
||||
if (!enabled && win.webContents.getZoomFactor() !== 1) win.webContents.setZoomFactor(1)
|
||||
updateZoom(win)
|
||||
}
|
||||
@@ -533,7 +533,7 @@ function wireZoom(win: BrowserWindow) {
|
||||
function wireFullscreen(win: BrowserWindow) {
|
||||
const send = (fullscreen: boolean) => {
|
||||
if (win.isDestroyed() || win.webContents.isDestroyed()) return
|
||||
win.webContents.send("window-fullscreen-changed", fullscreen)
|
||||
sendIpcEvent(win.webContents, Ipc.window.fullscreenChanged, fullscreen)
|
||||
}
|
||||
|
||||
win.on("enter-full-screen", () => send(true))
|
||||
@@ -546,7 +546,7 @@ function clampZoom(value: number) {
|
||||
|
||||
function updateZoom(win: BrowserWindow) {
|
||||
updateTitlebar(win)
|
||||
win.webContents.send("zoom-factor-changed", win.webContents.getZoomFactor())
|
||||
sendIpcEvent(win.webContents, Ipc.window.zoomFactorChanged, win.webContents.getZoomFactor())
|
||||
}
|
||||
|
||||
function upsertKeyValue(obj: Record<string, any>, keyToChange: string, value: any) {
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
import { app, ipcMain } from "electron"
|
||||
import type { IpcMainInvokeEvent } from "electron"
|
||||
import { app } from "electron"
|
||||
import type { WebContents } from "electron"
|
||||
import type { WslServerConfig, WslServersState } from "@opencode-ai/app/wsl/types"
|
||||
import { Ipc, sendIpcEvent } from "../../shared/ipc-contract"
|
||||
import type { WslServersController } from "./servers"
|
||||
import type { WslServersState } from "../../preload/types"
|
||||
import { nativeT } from "../native-translations"
|
||||
|
||||
export function registerWslIpcHandlers(controller?: WslServersController) {
|
||||
if (!controller) {
|
||||
registerUnavailableWslIpcHandlers()
|
||||
return
|
||||
}
|
||||
export type WslIpc = {
|
||||
subscribe(sender: WebContents): void
|
||||
unsubscribe(id: number): void
|
||||
getState(): WslServersState
|
||||
probeRuntime(): Promise<void>
|
||||
refreshDistros(): Promise<void>
|
||||
installWsl(): Promise<void>
|
||||
installDistro(value: string): Promise<void>
|
||||
probeAddable(value: string[]): Promise<void>
|
||||
installOpencode(value: string): Promise<void>
|
||||
openTerminal(value: string): Promise<void>
|
||||
addServer(value: string): Promise<WslServerConfig>
|
||||
removeServer(value: string): Promise<void>
|
||||
startServer(value: string): Promise<void>
|
||||
}
|
||||
|
||||
export function createWslIpc(controller?: WslServersController): WslIpc {
|
||||
if (!controller) return createUnavailableWslIpc()
|
||||
|
||||
const subscriptions = new Map<number, () => void>()
|
||||
const unsubscribe = (id: number) => {
|
||||
@@ -23,62 +37,38 @@ export function registerWslIpcHandlers(controller?: WslServersController) {
|
||||
subscriptions.clear()
|
||||
})
|
||||
|
||||
ipcMain.handle("wsl-servers-subscribe", (event) => {
|
||||
const id = event.sender.id
|
||||
if (subscriptions.has(id)) return
|
||||
subscriptions.set(
|
||||
id,
|
||||
controller.subscribe((payload) => {
|
||||
if (event.sender.isDestroyed()) {
|
||||
unsubscribe(id)
|
||||
return
|
||||
}
|
||||
event.sender.send("wsl-servers-event", payload)
|
||||
}),
|
||||
)
|
||||
event.sender.once("destroyed", () => unsubscribe(id))
|
||||
})
|
||||
ipcMain.handle("wsl-servers-unsubscribe", (event) => unsubscribe(event.sender.id))
|
||||
ipcMain.handle("wsl-servers-get-state", () => controller.getState())
|
||||
ipcMain.handle("wsl-servers-probe-runtime", () => controller.probeRuntime())
|
||||
ipcMain.handle("wsl-servers-refresh-distros", () => controller.refreshDistros())
|
||||
ipcMain.handle("wsl-servers-install-wsl", () => controller.installWsl())
|
||||
ipcMain.handle("wsl-servers-install-distro", (_event: IpcMainInvokeEvent, name: string) =>
|
||||
controller.installDistro(requireWslIpcString("distro", name)),
|
||||
)
|
||||
ipcMain.handle("wsl-servers-probe-addable", (_event: IpcMainInvokeEvent, distros: string[]) =>
|
||||
controller.probeAddable(requireWslIpcStrings("distro", distros)),
|
||||
)
|
||||
ipcMain.handle("wsl-servers-install-opencode", (_event: IpcMainInvokeEvent, name: string) =>
|
||||
controller.installOpencode(requireWslIpcString("distro", name)),
|
||||
)
|
||||
ipcMain.handle("wsl-servers-open-terminal", (_event: IpcMainInvokeEvent, name: string) =>
|
||||
controller.openTerminal(requireWslIpcString("distro", name)),
|
||||
)
|
||||
ipcMain.handle("wsl-servers-add", (_event: IpcMainInvokeEvent, distro: string) =>
|
||||
controller.addServer(requireWslIpcString("distro", distro)),
|
||||
)
|
||||
ipcMain.handle("wsl-servers-remove", (_event: IpcMainInvokeEvent, id: string) =>
|
||||
controller.removeServer(requireWslIpcString("server id", id)),
|
||||
)
|
||||
ipcMain.handle("wsl-servers-start", (_event: IpcMainInvokeEvent, id: string) =>
|
||||
controller.startServer(requireWslIpcString("server id", id)),
|
||||
)
|
||||
return {
|
||||
subscribe(sender) {
|
||||
const id = sender.id
|
||||
if (subscriptions.has(id)) return
|
||||
subscriptions.set(
|
||||
id,
|
||||
controller.subscribe((payload) => {
|
||||
if (sender.isDestroyed()) {
|
||||
unsubscribe(id)
|
||||
return
|
||||
}
|
||||
sendIpcEvent(sender, Ipc.wsl.event, payload)
|
||||
}),
|
||||
)
|
||||
sender.once("destroyed", () => unsubscribe(id))
|
||||
},
|
||||
unsubscribe,
|
||||
getState: () => controller.getState(),
|
||||
probeRuntime: () => controller.probeRuntime(),
|
||||
refreshDistros: () => controller.refreshDistros(),
|
||||
installWsl: () => controller.installWsl(),
|
||||
installDistro: (value) => controller.installDistro(requireWslIpcString("distro", value)),
|
||||
probeAddable: (value) => controller.probeAddable(requireWslIpcStrings("distro", value)),
|
||||
installOpencode: (value) => controller.installOpencode(requireWslIpcString("distro", value)),
|
||||
openTerminal: (value) => controller.openTerminal(requireWslIpcString("distro", value)),
|
||||
addServer: (value) => controller.addServer(requireWslIpcString("distro", value)),
|
||||
removeServer: (value) => controller.removeServer(requireWslIpcString("server id", value)),
|
||||
startServer: (value) => controller.startServer(requireWslIpcString("server id", value)),
|
||||
}
|
||||
}
|
||||
|
||||
function requireWslIpcString(name: string, value: unknown) {
|
||||
if (typeof value === "string" && value.length > 0) return value
|
||||
throw new Error(`Invalid ${name}`)
|
||||
}
|
||||
|
||||
function requireWslIpcStrings(name: string, value: unknown) {
|
||||
if (!Array.isArray(value)) throw new Error(`Invalid ${name}`)
|
||||
const values = value.map((item) => requireWslIpcString(name, item))
|
||||
if (values.length) return values
|
||||
throw new Error(`Invalid ${name}`)
|
||||
}
|
||||
|
||||
function registerUnavailableWslIpcHandlers() {
|
||||
function createUnavailableWslIpc(): WslIpc {
|
||||
const unavailable = () => {
|
||||
throw new Error(nativeT("desktop.wsl.error.windowsOnly"))
|
||||
}
|
||||
@@ -97,19 +87,31 @@ function registerUnavailableWslIpcHandlers() {
|
||||
job: null,
|
||||
})
|
||||
|
||||
ipcMain.handle("wsl-servers-subscribe", (event) => {
|
||||
event.sender.send("wsl-servers-event", { type: "state", state: state() })
|
||||
})
|
||||
ipcMain.handle("wsl-servers-unsubscribe", () => undefined)
|
||||
ipcMain.handle("wsl-servers-get-state", () => state())
|
||||
ipcMain.handle("wsl-servers-probe-runtime", unavailable)
|
||||
ipcMain.handle("wsl-servers-refresh-distros", unavailable)
|
||||
ipcMain.handle("wsl-servers-install-wsl", unavailable)
|
||||
ipcMain.handle("wsl-servers-install-distro", unavailable)
|
||||
ipcMain.handle("wsl-servers-probe-addable", unavailable)
|
||||
ipcMain.handle("wsl-servers-install-opencode", unavailable)
|
||||
ipcMain.handle("wsl-servers-open-terminal", unavailable)
|
||||
ipcMain.handle("wsl-servers-add", unavailable)
|
||||
ipcMain.handle("wsl-servers-remove", unavailable)
|
||||
ipcMain.handle("wsl-servers-start", unavailable)
|
||||
return {
|
||||
subscribe: (sender) => sendIpcEvent(sender, Ipc.wsl.event, { type: "state", state: state() }),
|
||||
unsubscribe: () => undefined,
|
||||
getState: state,
|
||||
probeRuntime: unavailable,
|
||||
refreshDistros: unavailable,
|
||||
installWsl: unavailable,
|
||||
installDistro: unavailable,
|
||||
probeAddable: unavailable,
|
||||
installOpencode: unavailable,
|
||||
openTerminal: unavailable,
|
||||
addServer: unavailable,
|
||||
removeServer: unavailable,
|
||||
startServer: unavailable,
|
||||
}
|
||||
}
|
||||
|
||||
function requireWslIpcString(name: string, value: unknown) {
|
||||
if (typeof value === "string" && value.length > 0) return value
|
||||
throw new Error(`Invalid ${name}`)
|
||||
}
|
||||
|
||||
function requireWslIpcStrings(name: string, value: unknown) {
|
||||
if (!Array.isArray(value)) throw new Error(`Invalid ${name}`)
|
||||
const values = value.map((item) => requireWslIpcString(name, item))
|
||||
if (values.length) return values
|
||||
throw new Error(`Invalid ${name}`)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process"
|
||||
import { existsSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
import * as pty from "@lydell/node-pty"
|
||||
import type { WslDistroProbe, WslInstalledDistro, WslOnlineDistro, WslRuntimeCheck } from "../../preload/types"
|
||||
import type { WslDistroProbe, WslInstalledDistro, WslOnlineDistro, WslRuntimeCheck } from "@opencode-ai/app/wsl/types"
|
||||
import { parseCliVersion } from "../cli-version"
|
||||
import { nativeT } from "../native-translations"
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import type { WslServerConfig } from "../../preload/types"
|
||||
import type { WslServerConfig } from "@opencode-ai/app/wsl/types"
|
||||
import { wslCliInstallCommand } from "./runtime"
|
||||
import { createWslServersController } from "./servers"
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
WslServerRuntime,
|
||||
WslServersEvent,
|
||||
WslServersState,
|
||||
} from "../../preload/types"
|
||||
} from "@opencode-ai/app/wsl/types"
|
||||
import { WSL_SERVERS_KEY } from "../store-keys"
|
||||
import { getStore } from "../store"
|
||||
import { nativeT } from "../native-translations"
|
||||
|
||||
@@ -1,133 +1,138 @@
|
||||
import { contextBridge, ipcRenderer, webUtils } from "electron"
|
||||
import type { ElectronAPI, WslServersEvent } from "./types"
|
||||
import type { IpcRendererEvent } from "electron"
|
||||
import type { ElectronAPI } from "./types"
|
||||
import type { UpdaterState } from "@opencode-ai/app/updater"
|
||||
import {
|
||||
Ipc,
|
||||
type IpcEvent,
|
||||
type IpcEventListener,
|
||||
type IpcInvoke,
|
||||
type IpcInvokeArgs,
|
||||
type IpcInvokeResult,
|
||||
type IpcSend,
|
||||
} from "../shared/ipc-contract"
|
||||
|
||||
function invoke<Channel extends keyof IpcInvoke>(channel: Channel, ...args: IpcInvokeArgs<Channel>) {
|
||||
return ipcRenderer.invoke(channel, ...args) as Promise<IpcInvokeResult<Channel>>
|
||||
}
|
||||
|
||||
function send<Channel extends keyof IpcSend>(channel: Channel, ...args: IpcSend[Channel]) {
|
||||
ipcRenderer.send(channel, ...args)
|
||||
}
|
||||
|
||||
function listen<Channel extends keyof IpcEvent>(channel: Channel, listener: IpcEventListener<Channel>) {
|
||||
const handler = (_event: IpcRendererEvent, ...args: IpcEvent[Channel]) => listener(...args)
|
||||
ipcRenderer.on(channel, handler)
|
||||
return () => ipcRenderer.removeListener(channel, handler)
|
||||
}
|
||||
|
||||
const updaterCallbacks = new Set<(state: UpdaterState) => void>()
|
||||
let updaterState: UpdaterState | undefined
|
||||
let updaterSubscription: Promise<void> | undefined
|
||||
const updaterHandler = (_: unknown, state: UpdaterState) => {
|
||||
let updaterListener: (() => void) | undefined
|
||||
const updaterHandler = (state: UpdaterState) => {
|
||||
updaterState = state
|
||||
updaterCallbacks.forEach((callback) => callback(state))
|
||||
}
|
||||
|
||||
const api: ElectronAPI = {
|
||||
awaitInitialization: () => ipcRenderer.invoke("await-initialization"),
|
||||
awaitInitialization: () => invoke(Ipc.app.awaitInitialization),
|
||||
wslServers: {
|
||||
getState: () => ipcRenderer.invoke("wsl-servers-get-state"),
|
||||
getState: () => invoke(Ipc.wsl.getState),
|
||||
subscribe: (cb) => {
|
||||
const handler = (_: unknown, event: WslServersEvent) => cb(event)
|
||||
ipcRenderer.on("wsl-servers-event", handler)
|
||||
void ipcRenderer.invoke("wsl-servers-subscribe")
|
||||
const dispose = listen(Ipc.wsl.event, cb)
|
||||
void invoke(Ipc.wsl.subscribe)
|
||||
return () => {
|
||||
ipcRenderer.removeListener("wsl-servers-event", handler)
|
||||
void ipcRenderer.invoke("wsl-servers-unsubscribe")
|
||||
dispose()
|
||||
void invoke(Ipc.wsl.unsubscribe)
|
||||
}
|
||||
},
|
||||
probeRuntime: () => ipcRenderer.invoke("wsl-servers-probe-runtime"),
|
||||
refreshDistros: () => ipcRenderer.invoke("wsl-servers-refresh-distros"),
|
||||
installWsl: () => ipcRenderer.invoke("wsl-servers-install-wsl"),
|
||||
installDistro: (name) => ipcRenderer.invoke("wsl-servers-install-distro", name),
|
||||
probeAddable: (distros) => ipcRenderer.invoke("wsl-servers-probe-addable", distros),
|
||||
installOpencode: (name) => ipcRenderer.invoke("wsl-servers-install-opencode", name),
|
||||
openTerminal: (name) => ipcRenderer.invoke("wsl-servers-open-terminal", name),
|
||||
addServer: (distro) => ipcRenderer.invoke("wsl-servers-add", distro),
|
||||
removeServer: (id) => ipcRenderer.invoke("wsl-servers-remove", id),
|
||||
startServer: (id) => ipcRenderer.invoke("wsl-servers-start", id),
|
||||
probeRuntime: () => invoke(Ipc.wsl.probeRuntime),
|
||||
refreshDistros: () => invoke(Ipc.wsl.refreshDistros),
|
||||
installWsl: () => invoke(Ipc.wsl.installWsl),
|
||||
installDistro: (name) => invoke(Ipc.wsl.installDistro, name),
|
||||
probeAddable: (distros) => invoke(Ipc.wsl.probeAddable, distros),
|
||||
installOpencode: (name) => invoke(Ipc.wsl.installOpencode, name),
|
||||
openTerminal: (name) => invoke(Ipc.wsl.openTerminal, name),
|
||||
addServer: (distro) => invoke(Ipc.wsl.addServer, distro),
|
||||
removeServer: (id) => invoke(Ipc.wsl.removeServer, id),
|
||||
startServer: (id) => invoke(Ipc.wsl.startServer, id),
|
||||
},
|
||||
updater: {
|
||||
subscribe: async (cb) => {
|
||||
updaterCallbacks.add(cb)
|
||||
if (updaterState) cb(updaterState)
|
||||
if (!updaterSubscription) {
|
||||
ipcRenderer.on("updater-state", updaterHandler)
|
||||
updaterSubscription = ipcRenderer.invoke("updater-subscribe")
|
||||
updaterListener = listen(Ipc.updater.state, updaterHandler)
|
||||
updaterSubscription = invoke(Ipc.updater.subscribe)
|
||||
}
|
||||
await updaterSubscription
|
||||
return () => {
|
||||
updaterCallbacks.delete(cb)
|
||||
if (updaterCallbacks.size > 0) return
|
||||
ipcRenderer.removeListener("updater-state", updaterHandler)
|
||||
updaterListener?.()
|
||||
updaterListener = undefined
|
||||
updaterSubscription = undefined
|
||||
void ipcRenderer.invoke("updater-unsubscribe")
|
||||
void invoke(Ipc.updater.unsubscribe)
|
||||
}
|
||||
},
|
||||
check: () => ipcRenderer.invoke("updater-check"),
|
||||
install: () => ipcRenderer.invoke("updater-install"),
|
||||
check: () => invoke(Ipc.updater.check),
|
||||
install: () => invoke(Ipc.updater.install),
|
||||
},
|
||||
consumeInitialDeepLinks: () => ipcRenderer.invoke("consume-initial-deep-links"),
|
||||
getDefaultServerUrl: () => ipcRenderer.invoke("get-default-server-url"),
|
||||
setDefaultServerUrl: (url) => ipcRenderer.invoke("set-default-server-url", url),
|
||||
isFirstLaunchOnboardingPending: () => ipcRenderer.invoke("is-first-launch-onboarding-pending"),
|
||||
consumeInitialDeepLinks: () => invoke(Ipc.app.consumeInitialDeepLinks),
|
||||
getDefaultServerUrl: () => invoke(Ipc.app.getDefaultServerUrl),
|
||||
setDefaultServerUrl: (url) => invoke(Ipc.app.setDefaultServerUrl, url),
|
||||
isFirstLaunchOnboardingPending: () => invoke(Ipc.app.isFirstLaunchOnboardingPending),
|
||||
finishFirstLaunchOnboarding: (createDefaultProject) =>
|
||||
ipcRenderer.invoke("finish-first-launch-onboarding", createDefaultProject),
|
||||
checkAppExists: (appName) => ipcRenderer.invoke("check-app-exists", appName),
|
||||
resolveAppPath: (appName) => ipcRenderer.invoke("resolve-app-path", appName),
|
||||
storeGet: (name, key) => ipcRenderer.invoke("store-get", name, key),
|
||||
storeSet: (name, key, value) => ipcRenderer.invoke("store-set", name, key, value),
|
||||
storeDelete: (name, key) => ipcRenderer.invoke("store-delete", name, key),
|
||||
storeClear: (name) => ipcRenderer.invoke("store-clear", name),
|
||||
storeKeys: (name) => ipcRenderer.invoke("store-keys", name),
|
||||
storeLength: (name) => ipcRenderer.invoke("store-length", name),
|
||||
draftGet: (key) => ipcRenderer.invoke("draft-get", key),
|
||||
draftSet: (key, value) => ipcRenderer.invoke("draft-set", key, value),
|
||||
draftDelete: (key) => ipcRenderer.invoke("draft-delete", key),
|
||||
draftBlobPut: (data) => ipcRenderer.invoke("draft-blob-put", data),
|
||||
draftBlobGet: (id) => ipcRenderer.invoke("draft-blob-get", id),
|
||||
invoke(Ipc.app.finishFirstLaunchOnboarding, createDefaultProject),
|
||||
checkAppExists: (appName) => invoke(Ipc.app.checkAppExists, appName),
|
||||
resolveAppPath: (appName) => invoke(Ipc.app.resolveAppPath, appName),
|
||||
storeGet: (name, key) => invoke(Ipc.storage.get, name, key),
|
||||
storeSet: (name, key, value) => invoke(Ipc.storage.set, name, key, value),
|
||||
storeDelete: (name, key) => invoke(Ipc.storage.delete, name, key),
|
||||
storeClear: (name) => invoke(Ipc.storage.clear, name),
|
||||
storeKeys: (name) => invoke(Ipc.storage.keys, name),
|
||||
storeLength: (name) => invoke(Ipc.storage.length, name),
|
||||
draftGet: (key) => invoke(Ipc.drafts.get, key),
|
||||
draftSet: (key, value) => invoke(Ipc.drafts.set, key, value),
|
||||
draftDelete: (key) => invoke(Ipc.drafts.delete, key),
|
||||
draftBlobPut: (data) => invoke(Ipc.drafts.putBlob, data),
|
||||
draftBlobGet: (id) => invoke(Ipc.drafts.getBlob, id),
|
||||
|
||||
getWindowID: () => ipcRenderer.invoke("get-window-id"),
|
||||
onMenuCommand: (cb) => {
|
||||
const handler = (_: unknown, id: string) => cb(id)
|
||||
ipcRenderer.on("menu-command", handler)
|
||||
return () => ipcRenderer.removeListener("menu-command", handler)
|
||||
},
|
||||
onDeepLink: (cb) => {
|
||||
const handler = (_: unknown, urls: string[]) => cb(urls)
|
||||
ipcRenderer.on("deep-link", handler)
|
||||
return () => ipcRenderer.removeListener("deep-link", handler)
|
||||
},
|
||||
getWindowID: () => invoke(Ipc.window.getId),
|
||||
onMenuCommand: (cb) => listen(Ipc.menu.command, cb),
|
||||
onDeepLink: (cb) => listen(Ipc.app.deepLink, cb),
|
||||
|
||||
openDirectoryPicker: (opts) => ipcRenderer.invoke("open-directory-picker", opts),
|
||||
openFilePicker: (opts) => ipcRenderer.invoke("open-file-picker", opts),
|
||||
readPickedFile: (token, path) => ipcRenderer.invoke("read-picked-file", token, path),
|
||||
releasePickedFiles: (token) => ipcRenderer.invoke("release-picked-files", token),
|
||||
openDirectoryPicker: (opts) => invoke(Ipc.files.openDirectoryPicker, opts),
|
||||
openFilePicker: (opts) => invoke(Ipc.files.openFilePicker, opts),
|
||||
readPickedFile: (token, path) => invoke(Ipc.files.readPickedFile, token, path),
|
||||
releasePickedFiles: (token) => invoke(Ipc.files.releasePickedFiles, token),
|
||||
getPathForFile: (file) => webUtils.getPathForFile(file),
|
||||
saveFilePicker: (opts) => ipcRenderer.invoke("save-file-picker", opts),
|
||||
openExternal: (url) => ipcRenderer.send("open-external", url),
|
||||
openLocalFile: (url) => ipcRenderer.send("open-local-file", url),
|
||||
openPath: (path, app) => ipcRenderer.invoke("open-path", path, app),
|
||||
revealPath: (path) => ipcRenderer.invoke("reveal-path", path),
|
||||
readClipboardImage: () => ipcRenderer.invoke("read-clipboard-image"),
|
||||
getWindowFocused: () => ipcRenderer.invoke("get-window-focused"),
|
||||
getWindowFullscreen: () => ipcRenderer.invoke("get-window-fullscreen"),
|
||||
onWindowFullscreenChanged: (cb) => {
|
||||
const handler = (_: unknown, fullscreen: boolean) => cb(fullscreen)
|
||||
ipcRenderer.on("window-fullscreen-changed", handler)
|
||||
return () => ipcRenderer.removeListener("window-fullscreen-changed", handler)
|
||||
},
|
||||
setWindowFocus: () => ipcRenderer.invoke("set-window-focus"),
|
||||
showWindow: () => ipcRenderer.invoke("show-window"),
|
||||
relaunch: () => ipcRenderer.send("relaunch"),
|
||||
getZoomFactor: () => ipcRenderer.invoke("get-zoom-factor"),
|
||||
setZoomFactor: (factor) => ipcRenderer.invoke("set-zoom-factor", factor),
|
||||
getPinchZoomEnabled: () => ipcRenderer.invoke("get-pinch-zoom-enabled"),
|
||||
setPinchZoomEnabled: (enabled) => ipcRenderer.invoke("set-pinch-zoom-enabled", enabled),
|
||||
onPinchZoomEnabledChanged: (cb) => {
|
||||
const handler = (_: unknown, enabled: boolean) => cb(enabled)
|
||||
ipcRenderer.on("pinch-zoom-enabled-changed", handler)
|
||||
return () => ipcRenderer.removeListener("pinch-zoom-enabled-changed", handler)
|
||||
},
|
||||
onZoomFactorChanged: (cb) => {
|
||||
const handler = (_: unknown, factor: number) => cb(factor)
|
||||
ipcRenderer.on("zoom-factor-changed", handler)
|
||||
return () => ipcRenderer.removeListener("zoom-factor-changed", handler)
|
||||
},
|
||||
setTitlebar: (theme) => ipcRenderer.invoke("set-titlebar", theme),
|
||||
runDesktopMenuAction: (action) => ipcRenderer.invoke("run-desktop-menu-action", action),
|
||||
setBackgroundColor: (color: string) => ipcRenderer.invoke("set-background-color", color),
|
||||
exportDebugLogs: () => ipcRenderer.invoke("export-debug-logs"),
|
||||
setForceFocus: (enabled) => ipcRenderer.invoke("set-force-focus", enabled),
|
||||
recordFatalRendererError: (error) => ipcRenderer.invoke("record-fatal-renderer-error", error),
|
||||
setNativeTranslations: (bundle) => ipcRenderer.invoke("set-native-translations", bundle),
|
||||
saveFilePicker: (opts) => invoke(Ipc.files.saveFilePicker, opts),
|
||||
openExternal: (url) => send(Ipc.files.openExternal, url),
|
||||
openLocalFile: (url) => send(Ipc.files.openLocalFile, url),
|
||||
openPath: (path, app) => invoke(Ipc.files.openPath, path, app),
|
||||
revealPath: (path) => invoke(Ipc.files.revealPath, path),
|
||||
readClipboardImage: () => invoke(Ipc.files.readClipboardImage),
|
||||
getWindowFocused: () => invoke(Ipc.window.getFocused),
|
||||
getWindowFullscreen: () => invoke(Ipc.window.getFullscreen),
|
||||
onWindowFullscreenChanged: (cb) => listen(Ipc.window.fullscreenChanged, cb),
|
||||
setWindowFocus: () => invoke(Ipc.window.setFocus),
|
||||
showWindow: () => invoke(Ipc.window.show),
|
||||
relaunch: () => send(Ipc.app.relaunch),
|
||||
getZoomFactor: () => invoke(Ipc.window.getZoomFactor),
|
||||
setZoomFactor: (factor) => invoke(Ipc.window.setZoomFactor, factor),
|
||||
getPinchZoomEnabled: () => invoke(Ipc.window.getPinchZoomEnabled),
|
||||
setPinchZoomEnabled: (enabled) => invoke(Ipc.window.setPinchZoomEnabled, enabled),
|
||||
onPinchZoomEnabledChanged: (cb) => listen(Ipc.window.pinchZoomEnabledChanged, cb),
|
||||
onZoomFactorChanged: (cb) => listen(Ipc.window.zoomFactorChanged, cb),
|
||||
setTitlebar: (theme) => invoke(Ipc.window.setTitlebar, theme),
|
||||
runDesktopMenuAction: (action) => invoke(Ipc.menu.runAction, action),
|
||||
setBackgroundColor: (color) => invoke(Ipc.app.setBackgroundColor, color),
|
||||
exportDebugLogs: () => invoke(Ipc.app.exportDebugLogs),
|
||||
setForceFocus: (enabled) => invoke(Ipc.app.setForceFocus, enabled),
|
||||
recordFatalRendererError: (error) => invoke(Ipc.app.recordFatalRendererError, error),
|
||||
setNativeTranslations: (bundle) => invoke(Ipc.app.setNativeTranslations, bundle),
|
||||
}
|
||||
|
||||
contextBridge.exposeInMainWorld("api", api)
|
||||
|
||||
@@ -1,110 +1,74 @@
|
||||
import type { DesktopMenuAction } from "@opencode-ai/app/desktop-menu"
|
||||
import type { WslServersPlatform } from "@opencode-ai/app/wsl/types"
|
||||
import type { UpdaterState } from "@opencode-ai/app/updater"
|
||||
import type { DesktopNativeBundle } from "@opencode-ai/app/i18n/desktop-native"
|
||||
export type {
|
||||
WslDistroProbe,
|
||||
WslInstalledDistro,
|
||||
WslJob,
|
||||
WslOnlineDistro,
|
||||
WslOpencodeCheck,
|
||||
WslRuntimeCheck,
|
||||
WslServerConfig,
|
||||
WslServerItem,
|
||||
WslServerRuntime,
|
||||
WslServersEvent,
|
||||
WslServersState,
|
||||
} from "@opencode-ai/app/wsl/types"
|
||||
|
||||
export type ServerReadyData = {
|
||||
url: string
|
||||
username: string | null
|
||||
password: string | null
|
||||
}
|
||||
import {
|
||||
Ipc,
|
||||
type IpcEventListener,
|
||||
type IpcEventSubscription,
|
||||
type IpcInvokeMethod,
|
||||
type IpcSendMethod,
|
||||
} from "../shared/ipc-contract"
|
||||
|
||||
export type WslServersAPI = WslServersPlatform
|
||||
export type UpdaterAPI = {
|
||||
subscribe: (cb: (state: UpdaterState) => void) => Promise<() => void>
|
||||
check: () => Promise<UpdaterState>
|
||||
install: () => Promise<void>
|
||||
}
|
||||
|
||||
export type TitlebarTheme = {
|
||||
mode: "light" | "dark"
|
||||
scheme?: "system" | "light" | "dark"
|
||||
}
|
||||
export type FatalRendererError = {
|
||||
error: string
|
||||
url: string
|
||||
version?: string
|
||||
platform: string
|
||||
os?: string
|
||||
subscribe: (cb: IpcEventListener<typeof Ipc.updater.state>) => Promise<() => void>
|
||||
check: IpcInvokeMethod<typeof Ipc.updater.check>
|
||||
install: IpcInvokeMethod<typeof Ipc.updater.install>
|
||||
}
|
||||
|
||||
export type ElectronAPI = {
|
||||
awaitInitialization: () => Promise<ServerReadyData>
|
||||
awaitInitialization: IpcInvokeMethod<typeof Ipc.app.awaitInitialization>
|
||||
wslServers: WslServersAPI
|
||||
updater: UpdaterAPI
|
||||
consumeInitialDeepLinks: () => Promise<string[]>
|
||||
getDefaultServerUrl: () => Promise<string | null>
|
||||
setDefaultServerUrl: (url: string | null) => Promise<void>
|
||||
isFirstLaunchOnboardingPending: () => Promise<boolean>
|
||||
finishFirstLaunchOnboarding: (createDefaultProject: boolean) => Promise<string | null>
|
||||
checkAppExists: (appName: string) => Promise<boolean>
|
||||
resolveAppPath: (appName: string) => Promise<string | null>
|
||||
storeGet: (name: string, key: string) => Promise<string | null>
|
||||
storeSet: (name: string, key: string, value: string) => Promise<void>
|
||||
storeDelete: (name: string, key: string) => Promise<void>
|
||||
storeClear: (name: string) => Promise<void>
|
||||
storeKeys: (name: string) => Promise<string[]>
|
||||
storeLength: (name: string) => Promise<number>
|
||||
draftGet: (key: string) => Promise<string | null>
|
||||
draftSet: (key: string, value: string) => Promise<void>
|
||||
draftDelete: (key: string) => Promise<void>
|
||||
draftBlobPut: (data: ArrayBuffer) => Promise<string>
|
||||
draftBlobGet: (id: string) => Promise<ArrayBuffer | null>
|
||||
consumeInitialDeepLinks: IpcInvokeMethod<typeof Ipc.app.consumeInitialDeepLinks>
|
||||
getDefaultServerUrl: IpcInvokeMethod<typeof Ipc.app.getDefaultServerUrl>
|
||||
setDefaultServerUrl: IpcInvokeMethod<typeof Ipc.app.setDefaultServerUrl>
|
||||
isFirstLaunchOnboardingPending: IpcInvokeMethod<typeof Ipc.app.isFirstLaunchOnboardingPending>
|
||||
finishFirstLaunchOnboarding: IpcInvokeMethod<typeof Ipc.app.finishFirstLaunchOnboarding>
|
||||
checkAppExists: IpcInvokeMethod<typeof Ipc.app.checkAppExists>
|
||||
resolveAppPath: IpcInvokeMethod<typeof Ipc.app.resolveAppPath>
|
||||
storeGet: IpcInvokeMethod<typeof Ipc.storage.get>
|
||||
storeSet: IpcInvokeMethod<typeof Ipc.storage.set>
|
||||
storeDelete: IpcInvokeMethod<typeof Ipc.storage.delete>
|
||||
storeClear: IpcInvokeMethod<typeof Ipc.storage.clear>
|
||||
storeKeys: IpcInvokeMethod<typeof Ipc.storage.keys>
|
||||
storeLength: IpcInvokeMethod<typeof Ipc.storage.length>
|
||||
draftGet: IpcInvokeMethod<typeof Ipc.drafts.get>
|
||||
draftSet: IpcInvokeMethod<typeof Ipc.drafts.set>
|
||||
draftDelete: IpcInvokeMethod<typeof Ipc.drafts.delete>
|
||||
draftBlobPut: IpcInvokeMethod<typeof Ipc.drafts.putBlob>
|
||||
draftBlobGet: IpcInvokeMethod<typeof Ipc.drafts.getBlob>
|
||||
|
||||
getWindowID: () => Promise<string>
|
||||
onMenuCommand: (cb: (id: string) => void) => () => void
|
||||
onDeepLink: (cb: (urls: string[]) => void) => () => void
|
||||
getWindowID: IpcInvokeMethod<typeof Ipc.window.getId>
|
||||
onMenuCommand: IpcEventSubscription<typeof Ipc.menu.command>
|
||||
onDeepLink: IpcEventSubscription<typeof Ipc.app.deepLink>
|
||||
|
||||
openDirectoryPicker: (opts?: {
|
||||
multiple?: boolean
|
||||
title?: string
|
||||
defaultPath?: string
|
||||
}) => Promise<string | string[] | null>
|
||||
openFilePicker: (opts?: {
|
||||
multiple?: boolean
|
||||
title?: string
|
||||
defaultPath?: string
|
||||
extensions?: string[]
|
||||
}) => Promise<{ token: string; files: { path: string; name: string; size: number }[] } | null>
|
||||
readPickedFile: (token: string, path: string) => Promise<ArrayBuffer>
|
||||
releasePickedFiles: (token: string) => Promise<void>
|
||||
openDirectoryPicker: IpcInvokeMethod<typeof Ipc.files.openDirectoryPicker>
|
||||
openFilePicker: IpcInvokeMethod<typeof Ipc.files.openFilePicker>
|
||||
readPickedFile: IpcInvokeMethod<typeof Ipc.files.readPickedFile>
|
||||
releasePickedFiles: IpcInvokeMethod<typeof Ipc.files.releasePickedFiles>
|
||||
getPathForFile: (file: File) => string
|
||||
saveFilePicker: (opts?: { title?: string; defaultPath?: string }) => Promise<string | null>
|
||||
openExternal: (url: string) => void
|
||||
openLocalFile: (url: string) => void
|
||||
openPath: (path: string, app?: string) => Promise<void>
|
||||
revealPath: (path: string) => Promise<boolean>
|
||||
readClipboardImage: () => Promise<{ buffer: ArrayBuffer; width: number; height: number } | null>
|
||||
getWindowFocused: () => Promise<boolean>
|
||||
getWindowFullscreen: () => Promise<boolean>
|
||||
onWindowFullscreenChanged: (cb: (fullscreen: boolean) => void) => () => void
|
||||
setWindowFocus: () => Promise<void>
|
||||
showWindow: () => Promise<void>
|
||||
relaunch: () => void
|
||||
getZoomFactor: () => Promise<number>
|
||||
setZoomFactor: (factor: number) => Promise<void>
|
||||
getPinchZoomEnabled: () => Promise<boolean>
|
||||
setPinchZoomEnabled: (enabled: boolean) => Promise<void>
|
||||
onPinchZoomEnabledChanged: (cb: (enabled: boolean) => void) => () => void
|
||||
onZoomFactorChanged: (cb: (factor: number) => void) => () => void
|
||||
setTitlebar: (theme: TitlebarTheme) => Promise<void>
|
||||
runDesktopMenuAction: (action: DesktopMenuAction) => Promise<void>
|
||||
setBackgroundColor: (color: string) => Promise<void>
|
||||
exportDebugLogs: () => Promise<string>
|
||||
setForceFocus: (enabled: boolean) => Promise<void>
|
||||
recordFatalRendererError: (error: FatalRendererError) => Promise<void>
|
||||
setNativeTranslations: (bundle: DesktopNativeBundle) => Promise<void>
|
||||
saveFilePicker: IpcInvokeMethod<typeof Ipc.files.saveFilePicker>
|
||||
openExternal: IpcSendMethod<typeof Ipc.files.openExternal>
|
||||
openLocalFile: IpcSendMethod<typeof Ipc.files.openLocalFile>
|
||||
openPath: IpcInvokeMethod<typeof Ipc.files.openPath>
|
||||
revealPath: IpcInvokeMethod<typeof Ipc.files.revealPath>
|
||||
readClipboardImage: IpcInvokeMethod<typeof Ipc.files.readClipboardImage>
|
||||
getWindowFocused: IpcInvokeMethod<typeof Ipc.window.getFocused>
|
||||
getWindowFullscreen: IpcInvokeMethod<typeof Ipc.window.getFullscreen>
|
||||
onWindowFullscreenChanged: IpcEventSubscription<typeof Ipc.window.fullscreenChanged>
|
||||
setWindowFocus: IpcInvokeMethod<typeof Ipc.window.setFocus>
|
||||
showWindow: IpcInvokeMethod<typeof Ipc.window.show>
|
||||
relaunch: IpcSendMethod<typeof Ipc.app.relaunch>
|
||||
getZoomFactor: IpcInvokeMethod<typeof Ipc.window.getZoomFactor>
|
||||
setZoomFactor: IpcInvokeMethod<typeof Ipc.window.setZoomFactor>
|
||||
getPinchZoomEnabled: IpcInvokeMethod<typeof Ipc.window.getPinchZoomEnabled>
|
||||
setPinchZoomEnabled: IpcInvokeMethod<typeof Ipc.window.setPinchZoomEnabled>
|
||||
onPinchZoomEnabledChanged: IpcEventSubscription<typeof Ipc.window.pinchZoomEnabledChanged>
|
||||
onZoomFactorChanged: IpcEventSubscription<typeof Ipc.window.zoomFactorChanged>
|
||||
setTitlebar: IpcInvokeMethod<typeof Ipc.window.setTitlebar>
|
||||
runDesktopMenuAction: IpcInvokeMethod<typeof Ipc.menu.runAction>
|
||||
setBackgroundColor: IpcInvokeMethod<typeof Ipc.app.setBackgroundColor>
|
||||
exportDebugLogs: IpcInvokeMethod<typeof Ipc.app.exportDebugLogs>
|
||||
setForceFocus: IpcInvokeMethod<typeof Ipc.app.setForceFocus>
|
||||
recordFatalRendererError: IpcInvokeMethod<typeof Ipc.app.recordFatalRendererError>
|
||||
setNativeTranslations: IpcInvokeMethod<typeof Ipc.app.setNativeTranslations>
|
||||
}
|
||||
|
||||
@@ -223,9 +223,10 @@ const createPlatform = (windowState: DesktopWindowState): Platform => {
|
||||
async openPath(path: string, app?: string) {
|
||||
if (os === "windows") {
|
||||
const resolvedApp = app ? await window.api.resolveAppPath(app).catch(() => null) : null
|
||||
return window.api.openPath(path, resolvedApp ?? undefined)
|
||||
await window.api.openPath(path, resolvedApp ?? undefined)
|
||||
return
|
||||
}
|
||||
return window.api.openPath(path, app)
|
||||
await window.api.openPath(path, app)
|
||||
},
|
||||
async revealPath(path: string) {
|
||||
return window.api.revealPath(path)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Ipc } from "../shared/ipc-contract"
|
||||
import { initializationData, initializationReady } from "./initialization"
|
||||
|
||||
describe("desktop renderer initialization", () => {
|
||||
@@ -16,7 +17,7 @@ describe("desktop renderer initialization", () => {
|
||||
|
||||
test("removes Electron's remote invocation wrapper from startup errors", () => {
|
||||
const error = new Error(
|
||||
"Error invoking remote method 'await-initialization': Error: Cannot migrate session_message projections",
|
||||
`Error invoking remote method '${Ipc.app.awaitInitialization}': Error: Cannot migrate session_message projections`,
|
||||
)
|
||||
|
||||
try {
|
||||
|
||||
@@ -5,7 +5,7 @@ export function initializationData<A>(state: (() => A | undefined) & { error: un
|
||||
|
||||
function markLocalServerStartup(error: unknown) {
|
||||
const failure = error instanceof Error ? error : new Error(String(error))
|
||||
const prefix = "Error invoking remote method 'await-initialization': Error: "
|
||||
const prefix = `Error invoking remote method '${Ipc.app.awaitInitialization}': Error: `
|
||||
if (failure.message.startsWith(prefix)) {
|
||||
const previous = failure.message
|
||||
failure.message = failure.message.slice(prefix.length)
|
||||
@@ -20,3 +20,4 @@ export function initializationReady<A>(state: (() => A | undefined) & { error: u
|
||||
initializationData(state)
|
||||
return true
|
||||
}
|
||||
import { Ipc } from "../shared/ipc-contract"
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useLanguage } from "@opencode-ai/app"
|
||||
import { LoaderV2 } from "@opencode-ai/ui/v2/loader-v2"
|
||||
import { showToastV2, toasterV2, ToastV2 } from "@opencode-ai/ui/v2/toast-v2"
|
||||
import { createRoot, createSignal, onCleanup, onMount } from "solid-js"
|
||||
import type { ServerReadyData } from "../preload/types"
|
||||
import type { ServerReadyData } from "../shared/ipc-contract"
|
||||
|
||||
type Progress = Extract<MigrationV1StatusOutput, { status: "running" }>["progress"]
|
||||
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
import type { DesktopMenuAction } from "@opencode-ai/app/desktop-menu"
|
||||
import type { DesktopNativeBundle } from "@opencode-ai/app/i18n/desktop-native"
|
||||
import type { UpdaterState } from "@opencode-ai/app/updater"
|
||||
import type { WslServerConfig, WslServersEvent, WslServersState } from "@opencode-ai/app/wsl/types"
|
||||
|
||||
export const Ipc = {
|
||||
app: {
|
||||
awaitInitialization: "await-initialization",
|
||||
consumeInitialDeepLinks: "consume-initial-deep-links",
|
||||
deepLink: "deep-link",
|
||||
getDefaultServerUrl: "get-default-server-url",
|
||||
setDefaultServerUrl: "set-default-server-url",
|
||||
isFirstLaunchOnboardingPending: "is-first-launch-onboarding-pending",
|
||||
finishFirstLaunchOnboarding: "finish-first-launch-onboarding",
|
||||
checkAppExists: "check-app-exists",
|
||||
resolveAppPath: "resolve-app-path",
|
||||
relaunch: "relaunch",
|
||||
setBackgroundColor: "set-background-color",
|
||||
exportDebugLogs: "export-debug-logs",
|
||||
setForceFocus: "set-force-focus",
|
||||
recordFatalRendererError: "record-fatal-renderer-error",
|
||||
setNativeTranslations: "set-native-translations",
|
||||
},
|
||||
storage: {
|
||||
get: "store-get",
|
||||
set: "store-set",
|
||||
delete: "store-delete",
|
||||
clear: "store-clear",
|
||||
keys: "store-keys",
|
||||
length: "store-length",
|
||||
},
|
||||
drafts: {
|
||||
get: "draft-get",
|
||||
set: "draft-set",
|
||||
delete: "draft-delete",
|
||||
putBlob: "draft-blob-put",
|
||||
getBlob: "draft-blob-get",
|
||||
},
|
||||
files: {
|
||||
openDirectoryPicker: "open-directory-picker",
|
||||
openFilePicker: "open-file-picker",
|
||||
readPickedFile: "read-picked-file",
|
||||
releasePickedFiles: "release-picked-files",
|
||||
saveFilePicker: "save-file-picker",
|
||||
openExternal: "open-external",
|
||||
openLocalFile: "open-local-file",
|
||||
openPath: "open-path",
|
||||
revealPath: "reveal-path",
|
||||
readClipboardImage: "read-clipboard-image",
|
||||
},
|
||||
window: {
|
||||
getId: "get-window-id",
|
||||
getFocused: "get-window-focused",
|
||||
getFullscreen: "get-window-fullscreen",
|
||||
fullscreenChanged: "window-fullscreen-changed",
|
||||
setFocus: "set-window-focus",
|
||||
show: "show-window",
|
||||
getZoomFactor: "get-zoom-factor",
|
||||
setZoomFactor: "set-zoom-factor",
|
||||
zoomFactorChanged: "zoom-factor-changed",
|
||||
getPinchZoomEnabled: "get-pinch-zoom-enabled",
|
||||
setPinchZoomEnabled: "set-pinch-zoom-enabled",
|
||||
pinchZoomEnabledChanged: "pinch-zoom-enabled-changed",
|
||||
setTitlebar: "set-titlebar",
|
||||
},
|
||||
menu: {
|
||||
command: "menu-command",
|
||||
runAction: "run-desktop-menu-action",
|
||||
},
|
||||
updater: {
|
||||
subscribe: "updater-subscribe",
|
||||
unsubscribe: "updater-unsubscribe",
|
||||
check: "updater-check",
|
||||
install: "updater-install",
|
||||
state: "updater-state",
|
||||
},
|
||||
wsl: {
|
||||
subscribe: "wsl-servers-subscribe",
|
||||
unsubscribe: "wsl-servers-unsubscribe",
|
||||
getState: "wsl-servers-get-state",
|
||||
probeRuntime: "wsl-servers-probe-runtime",
|
||||
refreshDistros: "wsl-servers-refresh-distros",
|
||||
installWsl: "wsl-servers-install-wsl",
|
||||
installDistro: "wsl-servers-install-distro",
|
||||
probeAddable: "wsl-servers-probe-addable",
|
||||
installOpencode: "wsl-servers-install-opencode",
|
||||
openTerminal: "wsl-servers-open-terminal",
|
||||
addServer: "wsl-servers-add",
|
||||
removeServer: "wsl-servers-remove",
|
||||
startServer: "wsl-servers-start",
|
||||
event: "wsl-servers-event",
|
||||
},
|
||||
} as const
|
||||
|
||||
export type ServerReadyData = {
|
||||
url: string
|
||||
username: string | null
|
||||
password: string | null
|
||||
}
|
||||
|
||||
export type TitlebarTheme = {
|
||||
mode: "light" | "dark"
|
||||
scheme?: "system" | "light" | "dark"
|
||||
}
|
||||
|
||||
export type FatalRendererError = {
|
||||
error: string
|
||||
url: string
|
||||
version?: string
|
||||
platform: string
|
||||
os?: string
|
||||
}
|
||||
|
||||
export type DirectoryPickerOptions = {
|
||||
multiple?: boolean
|
||||
title?: string
|
||||
defaultPath?: string
|
||||
}
|
||||
|
||||
export type FilePickerOptions = DirectoryPickerOptions & {
|
||||
extensions?: string[]
|
||||
}
|
||||
|
||||
export type PickedFiles = {
|
||||
token: string
|
||||
files: { path: string; name: string; size: number }[]
|
||||
}
|
||||
|
||||
export type SaveFilePickerOptions = {
|
||||
title?: string
|
||||
defaultPath?: string
|
||||
}
|
||||
|
||||
export type ClipboardImage = {
|
||||
buffer: ArrayBuffer
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export type IpcInvoke = {
|
||||
[Ipc.app.awaitInitialization]: { args: []; result: ServerReadyData }
|
||||
[Ipc.app.consumeInitialDeepLinks]: { args: []; result: string[] }
|
||||
[Ipc.app.getDefaultServerUrl]: { args: []; result: string | null }
|
||||
[Ipc.app.setDefaultServerUrl]: { args: [url: string | null]; result: void }
|
||||
[Ipc.app.isFirstLaunchOnboardingPending]: { args: []; result: boolean }
|
||||
[Ipc.app.finishFirstLaunchOnboarding]: { args: [createDefaultProject: boolean]; result: string | null }
|
||||
[Ipc.app.checkAppExists]: { args: [appName: string]; result: boolean }
|
||||
[Ipc.app.resolveAppPath]: { args: [appName: string]; result: string | null }
|
||||
[Ipc.app.setBackgroundColor]: { args: [color: string]; result: void }
|
||||
[Ipc.app.exportDebugLogs]: { args: []; result: string }
|
||||
[Ipc.app.setForceFocus]: { args: [enabled: boolean]; result: void }
|
||||
[Ipc.app.recordFatalRendererError]: { args: [error: FatalRendererError]; result: void }
|
||||
[Ipc.app.setNativeTranslations]: { args: [bundle: DesktopNativeBundle]; result: void }
|
||||
|
||||
[Ipc.storage.get]: { args: [name: string, key: string]; result: string | null }
|
||||
[Ipc.storage.set]: { args: [name: string, key: string, value: string]; result: void }
|
||||
[Ipc.storage.delete]: { args: [name: string, key: string]; result: void }
|
||||
[Ipc.storage.clear]: { args: [name: string]; result: void }
|
||||
[Ipc.storage.keys]: { args: [name: string]; result: string[] }
|
||||
[Ipc.storage.length]: { args: [name: string]; result: number }
|
||||
|
||||
[Ipc.drafts.get]: { args: [key: string]; result: string | null }
|
||||
[Ipc.drafts.set]: { args: [key: string, value: string]; result: void }
|
||||
[Ipc.drafts.delete]: { args: [key: string]; result: void }
|
||||
[Ipc.drafts.putBlob]: { args: [data: ArrayBuffer]; result: string }
|
||||
[Ipc.drafts.getBlob]: { args: [id: string]; result: ArrayBuffer | null }
|
||||
|
||||
[Ipc.files.openDirectoryPicker]: {
|
||||
args: [options?: DirectoryPickerOptions]
|
||||
result: string | string[] | null
|
||||
}
|
||||
[Ipc.files.openFilePicker]: { args: [options?: FilePickerOptions]; result: PickedFiles | null }
|
||||
[Ipc.files.readPickedFile]: { args: [token: string, path: string]; result: ArrayBuffer }
|
||||
[Ipc.files.releasePickedFiles]: { args: [token: string]; result: void }
|
||||
[Ipc.files.saveFilePicker]: { args: [options?: SaveFilePickerOptions]; result: string | null }
|
||||
[Ipc.files.openPath]: { args: [path: string, app?: string]; result: string | undefined }
|
||||
[Ipc.files.revealPath]: { args: [path: string]; result: boolean }
|
||||
[Ipc.files.readClipboardImage]: { args: []; result: ClipboardImage | null }
|
||||
|
||||
[Ipc.window.getId]: { args: []; result: string }
|
||||
[Ipc.window.getFocused]: { args: []; result: boolean }
|
||||
[Ipc.window.getFullscreen]: { args: []; result: boolean }
|
||||
[Ipc.window.setFocus]: { args: []; result: void }
|
||||
[Ipc.window.show]: { args: []; result: void }
|
||||
[Ipc.window.getZoomFactor]: { args: []; result: number }
|
||||
[Ipc.window.setZoomFactor]: { args: [factor: number]; result: void }
|
||||
[Ipc.window.getPinchZoomEnabled]: { args: []; result: boolean }
|
||||
[Ipc.window.setPinchZoomEnabled]: { args: [enabled: boolean]; result: void }
|
||||
[Ipc.window.setTitlebar]: { args: [theme: TitlebarTheme]; result: void }
|
||||
[Ipc.menu.runAction]: { args: [action: DesktopMenuAction]; result: void }
|
||||
|
||||
[Ipc.updater.subscribe]: { args: []; result: void }
|
||||
[Ipc.updater.unsubscribe]: { args: []; result: void }
|
||||
[Ipc.updater.check]: { args: []; result: UpdaterState }
|
||||
[Ipc.updater.install]: { args: []; result: void }
|
||||
|
||||
[Ipc.wsl.subscribe]: { args: []; result: void }
|
||||
[Ipc.wsl.unsubscribe]: { args: []; result: void }
|
||||
[Ipc.wsl.getState]: { args: []; result: WslServersState }
|
||||
[Ipc.wsl.probeRuntime]: { args: []; result: void }
|
||||
[Ipc.wsl.refreshDistros]: { args: []; result: void }
|
||||
[Ipc.wsl.installWsl]: { args: []; result: void }
|
||||
[Ipc.wsl.installDistro]: { args: [name: string]; result: void }
|
||||
[Ipc.wsl.probeAddable]: { args: [distros: string[]]; result: void }
|
||||
[Ipc.wsl.installOpencode]: { args: [name: string]; result: void }
|
||||
[Ipc.wsl.openTerminal]: { args: [name: string]; result: void }
|
||||
[Ipc.wsl.addServer]: { args: [distro: string]; result: WslServerConfig }
|
||||
[Ipc.wsl.removeServer]: { args: [id: string]; result: void }
|
||||
[Ipc.wsl.startServer]: { args: [id: string]; result: void }
|
||||
}
|
||||
|
||||
export type IpcSend = {
|
||||
[Ipc.app.relaunch]: []
|
||||
[Ipc.files.openExternal]: [url: string]
|
||||
[Ipc.files.openLocalFile]: [url: string]
|
||||
}
|
||||
|
||||
export type IpcEvent = {
|
||||
[Ipc.app.deepLink]: [urls: string[]]
|
||||
[Ipc.menu.command]: [id: string]
|
||||
[Ipc.updater.state]: [state: UpdaterState]
|
||||
[Ipc.wsl.event]: [event: WslServersEvent]
|
||||
[Ipc.window.fullscreenChanged]: [fullscreen: boolean]
|
||||
[Ipc.window.pinchZoomEnabledChanged]: [enabled: boolean]
|
||||
[Ipc.window.zoomFactorChanged]: [factor: number]
|
||||
}
|
||||
|
||||
export type IpcInvokeArgs<Channel extends keyof IpcInvoke> = IpcInvoke[Channel]["args"]
|
||||
export type IpcInvokeResult<Channel extends keyof IpcInvoke> = IpcInvoke[Channel]["result"]
|
||||
export type IpcInvokeMethod<Channel extends keyof IpcInvoke> = (
|
||||
...args: IpcInvokeArgs<Channel>
|
||||
) => Promise<IpcInvokeResult<Channel>>
|
||||
export type IpcSendMethod<Channel extends keyof IpcSend> = (...args: IpcSend[Channel]) => void
|
||||
export type IpcEventListener<Channel extends keyof IpcEvent> = (...args: IpcEvent[Channel]) => void
|
||||
export type IpcEventSubscription<Channel extends keyof IpcEvent> = (listener: IpcEventListener<Channel>) => () => void
|
||||
|
||||
type IpcEventSender = {
|
||||
send<Channel extends keyof IpcEvent>(channel: Channel, ...args: IpcEvent[Channel]): void
|
||||
}
|
||||
|
||||
export function sendIpcEvent<Channel extends keyof IpcEvent>(
|
||||
sender: IpcEventSender,
|
||||
channel: Channel,
|
||||
...args: IpcEvent[Channel]
|
||||
) {
|
||||
sender.send(channel, ...args)
|
||||
}
|
||||
Reference in New Issue
Block a user