mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-22 11:55:46 +00:00
refactor: scaffold Electron local server config
This commit is contained in:
@@ -6,5 +6,5 @@ export const CHANNEL: Channel = raw === "dev" || raw === "beta" || raw === "prod
|
||||
|
||||
export const SETTINGS_STORE = "opencode.settings"
|
||||
export const DEFAULT_SERVER_URL_KEY = "defaultServerUrl"
|
||||
export const WSL_ENABLED_KEY = "wslEnabled"
|
||||
export const LOCAL_SERVER_KEY = "localServer"
|
||||
export const UPDATER_ENABLED = app.isPackaged && CHANNEL !== "dev"
|
||||
|
||||
@@ -34,14 +34,15 @@ app.setAppUserModelId(appId)
|
||||
app.setPath("userData", join(app.getPath("appData"), appId))
|
||||
const { autoUpdater } = pkg
|
||||
|
||||
import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types"
|
||||
import type { InitStep, ServerReadyData, SqliteMigrationProgress } from "../preload/types"
|
||||
import { checkAppExists, resolveAppPath, wslPath } from "./apps"
|
||||
import { CHANNEL, UPDATER_ENABLED } from "./constants"
|
||||
import { registerIpcHandlers, sendDeepLinks, sendMenuCommand, sendSqliteMigrationProgress } from "./ipc"
|
||||
import { createLocalServerController } from "./local-server"
|
||||
import { initLogging } from "./logging"
|
||||
import { parseMarkdown } from "./markdown"
|
||||
import { createMenu } from "./menu"
|
||||
import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server"
|
||||
import { getDefaultServerUrl, setDefaultServerUrl, spawnLocalServer } from "./server"
|
||||
import { createLoadingWindow, createMainWindow, setBackgroundColor, setDockIcon } from "./windows"
|
||||
import type { Server } from "virtual:opencode-server"
|
||||
|
||||
@@ -55,6 +56,7 @@ const loadingComplete = defer<void>()
|
||||
const pendingDeepLinks: string[] = []
|
||||
|
||||
const serverReady = defer<ServerReadyData>()
|
||||
const localServer = createLocalServerController()
|
||||
const logger = initLogging()
|
||||
|
||||
logger.log("app starting", {
|
||||
@@ -231,10 +233,11 @@ registerIpcHandlers({
|
||||
initEmitter.off("step", listener)
|
||||
}
|
||||
},
|
||||
getLocalServerState: () => localServer.getState(),
|
||||
setLocalServerConfig: (config) => localServer.setConfig(config),
|
||||
onLocalServerEvent: (listener) => localServer.subscribe(listener),
|
||||
getDefaultServerUrl: () => getDefaultServerUrl(),
|
||||
setDefaultServerUrl: (url) => setDefaultServerUrl(url),
|
||||
getWslConfig: () => Promise.resolve(getWslConfig()),
|
||||
setWslConfig: (config: WslConfig) => setWslConfig(config),
|
||||
getDisplayBackend: async () => null,
|
||||
setDisplayBackend: async () => undefined,
|
||||
parseMarkdown: async (markdown) => parseMarkdown(markdown),
|
||||
|
||||
@@ -2,7 +2,15 @@ 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,
|
||||
LocalServerConfig,
|
||||
LocalServerEvent,
|
||||
LocalServerState,
|
||||
ServerReadyData,
|
||||
SqliteMigrationProgress,
|
||||
TitlebarTheme,
|
||||
} from "../preload/types"
|
||||
import { getStore } from "./store"
|
||||
import { setTitlebar } from "./windows"
|
||||
|
||||
@@ -14,10 +22,11 @@ const pickerFilters = (ext?: string[]) => {
|
||||
type Deps = {
|
||||
killSidecar: () => void
|
||||
awaitInitialization: (sendStep: (step: InitStep) => void) => Promise<ServerReadyData>
|
||||
getLocalServerState: () => Promise<LocalServerState> | LocalServerState
|
||||
setLocalServerConfig: (config: LocalServerConfig) => Promise<void> | void
|
||||
onLocalServerEvent: (listener: (event: LocalServerEvent) => void) => () => void
|
||||
getDefaultServerUrl: () => Promise<string | null> | string | null
|
||||
setDefaultServerUrl: (url: string | null) => Promise<void> | void
|
||||
getWslConfig: () => Promise<WslConfig>
|
||||
setWslConfig: (config: WslConfig) => Promise<void> | void
|
||||
getDisplayBackend: () => Promise<string | null>
|
||||
setDisplayBackend: (backend: string | null) => Promise<void> | void
|
||||
parseMarkdown: (markdown: string) => Promise<string> | string
|
||||
@@ -32,17 +41,27 @@ type Deps = {
|
||||
}
|
||||
|
||||
export function registerIpcHandlers(deps: Deps) {
|
||||
const offLocalServer = deps.onLocalServerEvent((payload) => {
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
if (win.isDestroyed()) continue
|
||||
win.webContents.send("local-server-event", payload)
|
||||
}
|
||||
})
|
||||
app.once("will-quit", offLocalServer)
|
||||
|
||||
ipcMain.handle("kill-sidecar", () => deps.killSidecar())
|
||||
ipcMain.handle("await-initialization", (event: IpcMainInvokeEvent) => {
|
||||
const send = (step: InitStep) => event.sender.send("init-step", step)
|
||||
return deps.awaitInitialization(send)
|
||||
})
|
||||
ipcMain.handle("local-server-get-state", () => deps.getLocalServerState())
|
||||
ipcMain.handle("local-server-set-config", (_event: IpcMainInvokeEvent, config: LocalServerConfig) =>
|
||||
deps.setLocalServerConfig(config),
|
||||
)
|
||||
ipcMain.handle("get-default-server-url", () => deps.getDefaultServerUrl())
|
||||
ipcMain.handle("set-default-server-url", (_event: IpcMainInvokeEvent, url: string | null) =>
|
||||
deps.setDefaultServerUrl(url),
|
||||
)
|
||||
ipcMain.handle("get-wsl-config", () => deps.getWslConfig())
|
||||
ipcMain.handle("set-wsl-config", (_event: IpcMainInvokeEvent, config: WslConfig) => deps.setWslConfig(config))
|
||||
ipcMain.handle("get-display-backend", () => deps.getDisplayBackend())
|
||||
ipcMain.handle("set-display-backend", (_event: IpcMainInvokeEvent, backend: string | null) =>
|
||||
deps.setDisplayBackend(backend),
|
||||
|
||||
116
packages/desktop-electron/src/main/local-server.ts
Normal file
116
packages/desktop-electron/src/main/local-server.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import type { LocalServerConfig, LocalServerEvent, LocalServerState, LocalServerStep } from "../preload/types"
|
||||
import { LOCAL_SERVER_KEY } from "./constants"
|
||||
import { store } from "./store"
|
||||
|
||||
export function defaultLocalServerConfig(): LocalServerConfig {
|
||||
return {
|
||||
mode: "windows",
|
||||
distro: null,
|
||||
onboarding: {
|
||||
step: null,
|
||||
complete: true,
|
||||
pendingRestart: false,
|
||||
},
|
||||
acknowledgements: {
|
||||
root: [],
|
||||
mismatch: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function createLocalServerController() {
|
||||
let state = toState(readLocalServerConfig())
|
||||
const listeners = new Set<(event: LocalServerEvent) => void>()
|
||||
|
||||
const emit = (event: LocalServerEvent) => {
|
||||
for (const listener of listeners) listener(event)
|
||||
}
|
||||
|
||||
return {
|
||||
getState() {
|
||||
return state
|
||||
},
|
||||
setConfig(config: LocalServerConfig) {
|
||||
const next = normalizeLocalServerConfig(config)
|
||||
store.set(LOCAL_SERVER_KEY, next)
|
||||
state = toState(next, state)
|
||||
emit({ type: "state", state })
|
||||
},
|
||||
subscribe(listener: (event: LocalServerEvent) => void) {
|
||||
listeners.add(listener)
|
||||
return () => listeners.delete(listener)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function readLocalServerConfig() {
|
||||
return normalizeLocalServerConfig(store.get(LOCAL_SERVER_KEY))
|
||||
}
|
||||
|
||||
function toState(config: LocalServerConfig, current?: LocalServerState): LocalServerState {
|
||||
return {
|
||||
config,
|
||||
runtime: {
|
||||
key: localServerKey(config),
|
||||
mode: config.mode,
|
||||
distro: config.distro,
|
||||
},
|
||||
status: current?.status ?? { kind: "idle" },
|
||||
job: current?.job ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeLocalServerConfig(value: unknown): LocalServerConfig {
|
||||
const fallback = defaultLocalServerConfig()
|
||||
if (!value || typeof value !== "object") return fallback
|
||||
const record = value as Record<string, unknown>
|
||||
const mode = record.mode === "wsl" ? "wsl" : "windows"
|
||||
const distro = typeof record.distro === "string" && record.distro.length > 0 ? record.distro : null
|
||||
return {
|
||||
mode,
|
||||
distro,
|
||||
onboarding: normalizeOnboarding(record.onboarding, mode),
|
||||
acknowledgements: normalizeAcknowledgements(record.acknowledgements),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeOnboarding(value: unknown, mode: LocalServerConfig["mode"]): LocalServerConfig["onboarding"] {
|
||||
const record = value && typeof value === "object" ? (value as Record<string, unknown>) : {}
|
||||
return {
|
||||
step: normalizeStep(record.step),
|
||||
complete: typeof record.complete === "boolean" ? record.complete : mode === "windows",
|
||||
pendingRestart: typeof record.pendingRestart === "boolean" ? record.pendingRestart : false,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAcknowledgements(value: unknown): LocalServerConfig["acknowledgements"] {
|
||||
const record = value && typeof value === "object" ? (value as Record<string, unknown>) : {}
|
||||
return {
|
||||
root: Array.isArray(record.root)
|
||||
? record.root.filter((item): item is string => typeof item === "string" && item.length > 0)
|
||||
: [],
|
||||
mismatch: Array.isArray(record.mismatch)
|
||||
? record.mismatch.flatMap((item) => {
|
||||
if (!item || typeof item !== "object") return []
|
||||
const path = typeof item.path === "string" ? item.path : ""
|
||||
const version = typeof item.version === "string" ? item.version : ""
|
||||
if (!path || !version) return []
|
||||
return [{ path, version }]
|
||||
})
|
||||
: [],
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeStep(value: unknown): LocalServerStep | null {
|
||||
if (value === "wsl") return value
|
||||
if (value === "distro") return value
|
||||
if (value === "opencode") return value
|
||||
if (value === "switch") return value
|
||||
return null
|
||||
}
|
||||
|
||||
function localServerKey(config: LocalServerConfig) {
|
||||
if (config.mode === "windows") return "local:windows"
|
||||
if (!config.distro) return "local:wsl"
|
||||
return `local:wsl:${config.distro}`
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import { app } from "electron"
|
||||
import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants"
|
||||
import { DEFAULT_SERVER_URL_KEY } from "./constants"
|
||||
import { getUserShell, loadShellEnv } from "./shell-env"
|
||||
import { getStore } from "./store"
|
||||
|
||||
export type WslConfig = { enabled: boolean }
|
||||
|
||||
export type HealthCheck = { wait: Promise<void> }
|
||||
|
||||
export function getDefaultServerUrl(): string | null {
|
||||
@@ -21,15 +19,6 @@ export function setDefaultServerUrl(url: string | null) {
|
||||
getStore().delete(DEFAULT_SERVER_URL_KEY)
|
||||
}
|
||||
|
||||
export function getWslConfig(): WslConfig {
|
||||
const value = getStore().get(WSL_ENABLED_KEY)
|
||||
return { enabled: typeof value === "boolean" ? value : false }
|
||||
}
|
||||
|
||||
export function setWslConfig(config: WslConfig) {
|
||||
getStore().set(WSL_ENABLED_KEY, config.enabled)
|
||||
}
|
||||
|
||||
export async function spawnLocalServer(hostname: string, port: number, password: string) {
|
||||
prepareServerEnv(password)
|
||||
const { Log, Server } = await import("virtual:opencode-server")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { contextBridge, ipcRenderer } from "electron"
|
||||
import type { ElectronAPI, InitStep, SqliteMigrationProgress } from "./types"
|
||||
import type { ElectronAPI, InitStep, LocalServerEvent, SqliteMigrationProgress } from "./types"
|
||||
|
||||
const api: ElectronAPI = {
|
||||
killSidecar: () => ipcRenderer.invoke("kill-sidecar"),
|
||||
@@ -11,10 +11,17 @@ const api: ElectronAPI = {
|
||||
ipcRenderer.removeListener("init-step", handler)
|
||||
})
|
||||
},
|
||||
localServer: {
|
||||
getState: () => ipcRenderer.invoke("local-server-get-state"),
|
||||
setConfig: (config) => ipcRenderer.invoke("local-server-set-config", config),
|
||||
subscribe: (cb) => {
|
||||
const handler = (_: unknown, event: LocalServerEvent) => cb(event)
|
||||
ipcRenderer.on("local-server-event", handler)
|
||||
return () => ipcRenderer.removeListener("local-server-event", handler)
|
||||
},
|
||||
},
|
||||
getDefaultServerUrl: () => ipcRenderer.invoke("get-default-server-url"),
|
||||
setDefaultServerUrl: (url) => ipcRenderer.invoke("set-default-server-url", url),
|
||||
getWslConfig: () => ipcRenderer.invoke("get-wsl-config"),
|
||||
setWslConfig: (config) => ipcRenderer.invoke("set-wsl-config", config),
|
||||
getDisplayBackend: () => ipcRenderer.invoke("get-display-backend"),
|
||||
setDisplayBackend: (backend) => ipcRenderer.invoke("set-display-backend", backend),
|
||||
parseMarkdownCommand: (markdown) => ipcRenderer.invoke("parse-markdown", markdown),
|
||||
|
||||
@@ -8,7 +8,49 @@ export type ServerReadyData = {
|
||||
|
||||
export type SqliteMigrationProgress = { type: "InProgress"; value: number } | { type: "Done" }
|
||||
|
||||
export type WslConfig = { enabled: boolean }
|
||||
export type LocalServerMode = "windows" | "wsl"
|
||||
export type LocalServerStep = "wsl" | "distro" | "opencode" | "switch"
|
||||
export type LocalServerMismatchAcknowledgement = {
|
||||
path: string
|
||||
version: string
|
||||
}
|
||||
export type LocalServerConfig = {
|
||||
mode: LocalServerMode
|
||||
distro: string | null
|
||||
onboarding: {
|
||||
step: LocalServerStep | null
|
||||
complete: boolean
|
||||
pendingRestart: boolean
|
||||
}
|
||||
acknowledgements: {
|
||||
root: string[]
|
||||
mismatch: LocalServerMismatchAcknowledgement[]
|
||||
}
|
||||
}
|
||||
export type LocalServerStatus =
|
||||
| { kind: "idle" }
|
||||
| { kind: "ready" }
|
||||
| { kind: "running"; step: LocalServerStep | null }
|
||||
| { kind: "failed"; step: LocalServerStep | null; message: string }
|
||||
export type LocalServerState = {
|
||||
config: LocalServerConfig
|
||||
runtime: {
|
||||
key: string
|
||||
mode: LocalServerMode
|
||||
distro: string | null
|
||||
}
|
||||
status: LocalServerStatus
|
||||
job: { step: LocalServerStep | null; startedAt: number } | null
|
||||
}
|
||||
export type LocalServerEvent = {
|
||||
type: "state"
|
||||
state: LocalServerState
|
||||
}
|
||||
export type LocalServerAPI = {
|
||||
getState: () => Promise<LocalServerState>
|
||||
setConfig: (config: LocalServerConfig) => Promise<void>
|
||||
subscribe: (cb: (event: LocalServerEvent) => void) => () => void
|
||||
}
|
||||
|
||||
export type LinuxDisplayBackend = "wayland" | "auto"
|
||||
export type TitlebarTheme = {
|
||||
@@ -19,10 +61,9 @@ export type ElectronAPI = {
|
||||
killSidecar: () => Promise<void>
|
||||
installCli: () => Promise<string>
|
||||
awaitInitialization: (onStep: (step: InitStep) => void) => Promise<ServerReadyData>
|
||||
localServer: LocalServerAPI
|
||||
getDefaultServerUrl: () => Promise<string | null>
|
||||
setDefaultServerUrl: (url: string | null) => Promise<void>
|
||||
getWslConfig: () => Promise<WslConfig>
|
||||
setWslConfig: (config: WslConfig) => Promise<void>
|
||||
getDisplayBackend: () => Promise<LinuxDisplayBackend | null>
|
||||
setDisplayBackend: (backend: LinuxDisplayBackend | null) => Promise<void>
|
||||
parseMarkdownCommand: (markdown: string) => Promise<string>
|
||||
|
||||
@@ -194,16 +194,6 @@ 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
|
||||
},
|
||||
|
||||
setWslEnabled: async (enabled) => {
|
||||
await window.api.setWslConfig({ enabled })
|
||||
},
|
||||
|
||||
getDefaultServer: async () => {
|
||||
const url = await window.api.getDefaultServerUrl().catch(() => null)
|
||||
if (!url) return null
|
||||
|
||||
Reference in New Issue
Block a user