perf(desktop): show the first window the moment Electron is ready (#49869)

This commit is contained in:
Luke Parker
2026-09-19 10:41:27 +10:00
committed by GitHub
parent c2b171d7c4
commit e1e2efe8ec
14 changed files with 291 additions and 145 deletions
+23 -4
View File
@@ -142,6 +142,14 @@ console.log(`service: ${service}, runs: ${runs} (+${warmup} warm-up), cdp ${cdpP
if (service === "warm") await warmService()
const samples: Sample[] = []
// A launch that never produces a renderer would otherwise leave an instance behind that every later
// launch hands off to through the single-instance lock.
process.on("uncaughtException", async (error) => {
console.error(error)
await killApp()
await stopService()
process.exit(1)
})
for (let run = 1 - warmup; run <= runs; run++) {
for (const build of builds) {
const sample = await launch(build, run)
@@ -516,11 +524,18 @@ function mainLog() {
const dirs = existsSync(paths.logs) ? readdirSync(paths.logs).sort().reverse() : []
const dir = dirs.map((d) => join(paths.logs, d)).find((d) => existsSync(join(d, "main.log")))
const timeline: [number, string, string][] = []
let windowShownAt: number | undefined
for (const name of dir ? readdirSync(dir).filter((f) => f.endsWith(".log")) : []) {
for (const line of readFileSync(join(dir!, name), "utf8").split(/\r?\n/)) {
const text = readFileSync(join(dir!, name), "utf8")
// electron-log wraps long objects onto continuation lines; read them as part of the entry.
for (const entry of text.split(/\r?\n(?=\[\d{4}-)/)) {
const line = entry.split(/\r?\n/)[0]
const m = line.match(/^\[(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\.\d{3})\]\s+\[\w+\]\s+(?:\([\w-]+\)\s+)?(.*)$/)
if (!m) continue
const message = m[2].replace(/\s*\{.*$/, "").trim()
// A window shown before the logger existed reports when it was shown; the line itself is later.
const shown = /main window visible/.test(message) ? entry.match(/shownAt: (\d+)/)?.[1] : undefined
if (shown) windowShownAt = Number(shown)
timeline.push([new Date(m[1].replace(" ", "T")).getTime(), name.replace(/\.log$/, ""), message])
}
}
@@ -533,7 +548,7 @@ function mainLog() {
versionDone: at(/v2 CLI command completed/),
serviceStarting: at(/v2 CLI background service starting/),
serviceReady: at(/background service ready/),
windowVisible: at(/main window visible/),
windowVisible: windowShownAt ?? at(/main window visible/),
}
}
@@ -592,8 +607,12 @@ function bundledCli(exe: string) {
async function warmService() {
await stopService()
const clis = builds.map((build) => bundledCli(build.exe))
if (new Set(clis.map((cli) => statSync(cli).size)).size > 1)
console.warn("warning: the compared builds bundle different CLIs; the desktop will restart the service on the mismatch")
const identity = (cli: string) => {
const version = join(dirname(cli), "opencode-cli.version")
return existsSync(version) ? readFileSync(version, "utf8").trim() : String(statSync(cli).size)
}
if (new Set(clis.map(identity)).size > 1)
throw new Error("The compared builds bundle different CLIs; the desktop would restart the service on the mismatch")
serviceProcess = spawn(clis[0], ["serve", "--service"], { env, detached: true, stdio: "ignore" })
serviceProcess.unref()
const deadline = Date.now() + 60_000
+14
View File
@@ -6,3 +6,17 @@ export const CHANNEL: Channel = raw === "local" || raw === "dev" || raw === "bet
export const VERSION = app.isPackaged ? app.getVersion() : (process.env.OPENCODE_VERSION ?? app.getVersion())
export const UPDATER_ENABLED = app.isPackaged && CHANNEL !== "dev"
const appNames: Record<string, string> = {
dev: "OpenCode Dev",
beta: "OpenCode Beta",
prod: "OpenCode",
}
const appIDs: Record<string, string> = {
dev: "ai.opencode.desktop.dev",
beta: "ai.opencode.desktop.beta",
prod: "ai.opencode.desktop",
}
// Local renderer/server mode keeps the dev application identity.
export const APP_NAME = app.isPackaged ? appNames[CHANNEL] : "OpenCode Dev"
export const APP_ID = app.isPackaged ? appIDs[CHANNEL] : "ai.opencode.desktop.dev"
+2
View File
@@ -3,6 +3,7 @@ import { app } from "electron"
import { Effect, Layer } from "effect"
import { Ipc } from "./ipc"
import { DesktopInitialization } from "./lifecycle/desktop-initialization"
import { installContextMenu } from "./lifecycle/environment"
import { ApplicationLifecycle } from "./lifecycle"
import { BackgroundService } from "./service/background-service"
import { DesktopCli } from "./service/desktop-cli"
@@ -12,6 +13,7 @@ const runIpc = Effect.fn("Desktop.runIpc")(function* () {
const lifecycle = yield* ApplicationLifecycle.Service
const ipc = yield* Ipc.registerIpcHandlers
if (lifecycle.restoreWindows().length) ipc.installMenu()
yield* installContextMenu
yield* Effect.callback<void>((resume) => {
const quit = () => resume(Effect.void)
app.once("will-quit", quit)
+18 -2
View File
@@ -1,3 +1,19 @@
export {}
import { app } from "electron"
import { acquireApplicationLock, configureApplication } from "./lifecycle/configure"
import { createEarlyWindow } from "./windows/early"
import { registerRendererScheme } from "./windows/scheme"
await import("./desktop")
// This module stays small on purpose. Electron holds the ready event until the entry module has
// finished, and the first window should be on screen before the rest of the main process — a few
// hundred milliseconds of module evaluation and layers — loads. Configuration and the scheme must
// precede ready; the window is created the moment ready fires; everything else is imported after.
configureApplication()
if (acquireApplicationLock()) {
registerRendererScheme()
// Window first, then the bundle: starting the import before ready delays ready itself, because the
// module graph evaluates on the same thread Chromium needs to finish initialising.
void app.whenReady().then(() => {
createEarlyWindow()
return import("./desktop")
})
}
@@ -0,0 +1,58 @@
import { randomUUID } from "node:crypto"
import { mkdirSync, rmSync } from "node:fs"
import { homedir, tmpdir } from "node:os"
import path from "node:path"
import { app } from "electron"
import { APP_ID, APP_NAME } from "../constants"
const testOnboarding = process.env.OPENCODE_TEST_ONBOARDING === "1"
const jsCallStackFeature = "DocumentPolicyIncludeJSCallStacksInCrashReports"
// Runs synchronously from the entry module, before Chromium is ready: command-line switches only
// take effect before ready, the single-instance lock is scoped to userData, and the early window
// needs userData to find the persisted window list and state.
export function configureApplication() {
try {
process.chdir(homedir())
} catch {}
process.env.OPENCODE_DISABLE_EMBEDDED_WEB_UI = "true"
app.setName(APP_NAME)
app.setAppUserModelId(APP_ID)
app.commandLine.appendSwitch("proxy-bypass-list", "<-loopback>")
const features = app.commandLine.getSwitchValue("enable-features")
app.commandLine.appendSwitch("enable-features", features ? `${jsCallStackFeature},${features}` : jsCallStackFeature)
if (!app.isPackaged)
app.commandLine.appendSwitch("remote-debugging-port", process.env.OPENCODE_DESKTOP_REMOTE_DEBUGGING_PORT ?? "9222")
const testRoot = createTestRoot()
app.setPath("userData", testRoot ? path.join(testRoot, "desktop") : path.join(app.getPath("appData"), APP_ID))
if (testRoot) {
app.setPath("sessionData", path.join(testRoot, "session"))
if (testOnboarding) app.setPath("documents", path.join(testRoot, "documents"))
}
}
export function acquireApplicationLock() {
if (app.requestSingleInstanceLock()) return true
app.quit()
return false
}
function createTestRoot() {
const root = testOnboarding
? path.join(tmpdir(), `opencode-onboarding-${randomUUID()}`)
: app.isPackaged
? undefined
: process.env.OPENCODE_DESKTOP_TEST_ROOT
if (!root) return undefined
if (testOnboarding) rmSync(root, { recursive: true, force: true })
for (const dir of ["data", "config", "cache", "state", "desktop", "session", "documents"])
mkdirSync(path.join(root, dir), { recursive: true })
if (testOnboarding) process.env.OPENCODE_DB = ":memory:"
process.env.XDG_DATA_HOME = path.join(root, "data")
process.env.XDG_CONFIG_HOME = path.join(root, "config")
process.env.XDG_CACHE_HOME = path.join(root, "cache")
process.env.XDG_STATE_HOME = path.join(root, "state")
return root
}
@@ -1,59 +1,18 @@
import { randomUUID } from "node:crypto"
import http from "node:http"
import { homedir, tmpdir } from "node:os"
import { getCACertificates, setDefaultCACertificates } from "node:tls"
import { app } from "electron"
import contextMenu from "electron-context-menu"
import { Effect, FileSystem, Path } from "effect"
import { CHANNEL } from "../constants"
import { Effect, Path } from "effect"
import { DesktopPaths } from "../paths"
import { getUserShell, loadShellEnv } from "../service/shell-env"
import { registerRendererProtocol, setDockIcon } from "../windows"
const appNames: Record<string, string> = {
dev: "OpenCode Dev",
beta: "OpenCode Beta",
prod: "OpenCode",
}
const appIDs: Record<string, string> = {
dev: "ai.opencode.desktop.dev",
beta: "ai.opencode.desktop.beta",
prod: "ai.opencode.desktop",
}
const testOnboarding = process.env.OPENCODE_TEST_ONBOARDING === "1"
const jsCallStackFeature = "DocumentPolicyIncludeJSCallStacksInCrashReports"
export const configureApplication = Effect.fn("Application.configure")(function* () {
const path = yield* Path.Path
// electron-context-menu attaches to every existing and future window, so it can load once the first
// window is up instead of holding up startup with its dependency tree.
export const installContextMenu = Effect.gen(function* () {
const { default: contextMenu } = yield* Effect.promise(() => import("electron-context-menu"))
contextMenu({ showSaveImageAs: true, showLookUpSelection: false, showSearchWithGoogle: false })
try {
process.chdir(homedir())
} catch {}
process.env.OPENCODE_DISABLE_EMBEDDED_WEB_UI = "true"
const appID = app.isPackaged ? appIDs[CHANNEL] : "ai.opencode.desktop.dev"
app.setName(app.isPackaged ? appNames[CHANNEL] : "OpenCode Dev")
app.setAppUserModelId(appID)
app.commandLine.appendSwitch("proxy-bypass-list", "<-loopback>")
const features = app.commandLine.getSwitchValue("enable-features")
app.commandLine.appendSwitch("enable-features", features ? `${jsCallStackFeature},${features}` : jsCallStackFeature)
if (!app.isPackaged)
app.commandLine.appendSwitch("remote-debugging-port", process.env.OPENCODE_DESKTOP_REMOTE_DEBUGGING_PORT ?? "9222")
const testRoot = yield* createTestRoot()
app.setPath("userData", testRoot ? path.join(testRoot, "desktop") : path.join(app.getPath("appData"), appID))
if (testRoot) {
app.setPath("sessionData", path.join(testRoot, "session"))
if (testOnboarding) app.setPath("documents", path.join(testRoot, "documents"))
}
})
export function acquireApplicationLock() {
if (app.requestSingleInstanceLock()) return true
app.quit()
return false
}
export const prepareApplicationEnvironment = Effect.gen(function* () {
yield* loadSystemCertificates
yield* loadProxyEnvironment
@@ -91,29 +50,6 @@ export const loadProxyEnvironment = Effect.gen(function* () {
}).pipe(Effect.catch((error) => Effect.logWarning("failed to load proxy environment", { error })))
})
const createTestRoot = Effect.fn("Application.createTestRoot")(function* () {
const fs = yield* FileSystem.FileSystem
const path = yield* Path.Path
const root = testOnboarding
? path.join(tmpdir(), `opencode-onboarding-${randomUUID()}`)
: app.isPackaged
? undefined
: process.env.OPENCODE_DESKTOP_TEST_ROOT
if (!root) return undefined
if (testOnboarding) yield* fs.remove(root, { recursive: true, force: true })
yield* Effect.forEach(
["data", "config", "cache", "state", "desktop", "session", "documents"],
(dir) => fs.makeDirectory(path.join(root, dir), { recursive: true }),
{ discard: true },
)
if (testOnboarding) process.env.OPENCODE_DB = ":memory:"
process.env.XDG_DATA_HOME = path.join(root, "data")
process.env.XDG_CONFIG_HOME = path.join(root, "config")
process.env.XDG_CACHE_HOME = path.join(root, "cache")
process.env.XDG_STATE_HOME = path.join(root, "state")
return root
})
const loadSystemCertificates = Effect.try({
try: () => {
setDefaultCACertificates([...new Set([...getCACertificates("default"), ...getCACertificates("system")])])
+1 -5
View File
@@ -9,7 +9,6 @@ import { DesktopLogging, scoped } from "../native/logging"
import { DesktopStorage } from "../storage"
import { safeWebContentsURL } from "../windows/state"
import { getLastFocusedWindow, makeMainWindows, setAppQuitting, setRelaunchHandler } from "../windows"
import { acquireApplicationLock, configureApplication } from "./environment"
import { initializeFirstLaunchOnboarding } from "./onboarding"
import { Shutdown } from "./shutdown"
@@ -145,7 +144,7 @@ const runtime = Layer.effect(
}),
)
// Storage opens after configureApplication has set userData and before windows exist, so window
// Storage opens after the entry module has set userData and before the renderer loads, so window
// teardown can clear a window's persisted state and every renderer request finds it ready.
const platform = Layer.mergeAll(
DesktopLogging.layer,
@@ -155,9 +154,6 @@ const platform = Layer.mergeAll(
export const layer = Layer.unwrap(
Effect.gen(function* () {
// Electron scopes the single-instance lock to userData.
yield* configureApplication()
if (!acquireApplicationLock()) return yield* Effect.interrupt
// Decide first-launch state before the storage layer creates drafts.sqlite, which would
// otherwise read as evidence of an earlier launch on a fresh install.
yield* initializeFirstLaunchOnboarding(app.getPath("userData"))
@@ -1,6 +1,3 @@
import { resolveThemeVariant } from "@opencode/ui/theme/resolve"
import type { DesktopTheme } from "@opencode/ui/theme/types"
import oc2ThemeJson from "../../../../ui/src/theme/themes/oc-2.json"
import { app, BrowserWindow, nativeImage, nativeTheme } from "electron"
import type { Path } from "effect"
import { type TitlebarTheme } from "../../shared/ipc-contract"
@@ -9,16 +6,10 @@ import { emitIpcEvent } from "../ipc-events"
import type { DesktopPaths } from "../paths"
import { BACKGROUND_COLOR_KEY, PINCH_ZOOM_ENABLED_KEY } from "../storage/keys"
import { getStore } from "../storage/store"
import { storedBackgroundColor, titlebarOverlay, tone } from "./defaults"
const oc2Theme = oc2ThemeJson as DesktopTheme
const oc2Background = {
light: resolveThemeVariant(oc2Theme.light, false)["background-base"],
dark: resolveThemeVariant(oc2Theme.dark, true)["background-base"],
}
const titlebarThemes = new WeakMap<BrowserWindow, Partial<TitlebarTheme>>()
const pinchZoomEnabled = new WeakMap<BrowserWindow, boolean>()
// Match the renderer's 36px titlebar plus its former 8px content inset.
const titlebarHeight = 44
const maxZoomLevel = 10
const minZoomLevel = 0.2
let backgroundColor: string | undefined
@@ -28,7 +19,7 @@ export function windowAppearance(path: Path.Path, paths: DesktopPaths.Resolved)
return {
title: "OpenCode",
icon: iconPath(path, paths),
backgroundColor: getBackgroundColor() ?? oc2Background[mode],
backgroundColor: backgroundColor ?? storedBackgroundColor(),
...(process.platform === "darwin"
? {
titleBarStyle: "hidden" as const,
@@ -139,17 +130,8 @@ function iconPath(path: Path.Path, paths: DesktopPaths.Resolved) {
return path.join(iconsDir(path, paths), `icon.${process.platform === "win32" ? "ico" : "png"}`)
}
function tone() {
return nativeTheme.shouldUseDarkColors ? "dark" : "light"
}
function overlay(theme: Partial<TitlebarTheme> = {}, zoom = 1) {
const mode = theme.mode ?? tone()
return {
color: "#00000000",
symbolColor: mode === "dark" ? "white" : "black",
height: Math.max(titlebarHeight, Math.round(titlebarHeight * zoom)),
}
return titlebarOverlay(theme.mode ?? tone(), zoom)
}
function clampZoom(value: number) {
@@ -0,0 +1,36 @@
import { resolveThemeVariant } from "@opencode/ui/theme/resolve"
import type { DesktopTheme } from "@opencode/ui/theme/types"
import { nativeTheme } from "electron"
import oc2ThemeJson from "../../../../ui/src/theme/themes/oc-2.json"
import { BACKGROUND_COLOR_KEY } from "../storage/keys"
import { getStore } from "../storage/store"
// Frame defaults shared by the early window (created on ready, before the renderer exists) and the
// full window setup in appearance.ts, so both draw the same frame.
const oc2Theme = oc2ThemeJson as DesktopTheme
const oc2Background = {
light: resolveThemeVariant(oc2Theme.light, false)["background-base"],
dark: resolveThemeVariant(oc2Theme.dark, true)["background-base"],
}
// Match the renderer's 36px titlebar plus its former 8px content inset.
export const titlebarHeight = 44
export function tone() {
return nativeTheme.shouldUseDarkColors ? "dark" : "light"
}
// The colour the renderer reported on its last run, or the default theme's for the system tone, so
// a window shown before the renderer paints already has the right background.
export function storedBackgroundColor() {
const stored = getStore().get(BACKGROUND_COLOR_KEY)
return typeof stored === "string" ? stored : oc2Background[tone()]
}
export function titlebarOverlay(mode: "light" | "dark" = tone(), zoom = 1) {
return {
color: "#00000000",
symbolColor: mode === "dark" ? "white" : "black",
height: Math.max(titlebarHeight, Math.round(titlebarHeight * zoom)),
}
}
@@ -0,0 +1,66 @@
import { randomUUID } from "node:crypto"
import path from "node:path"
import { fileURLToPath } from "node:url"
import { app, BrowserWindow, screen } from "electron"
import { windowIDArgument } from "../../shared/window-bootstrap"
import { WINDOW_IDS_KEY } from "../storage/keys"
import { getStore } from "../storage/store"
import { storedBackgroundColor, titlebarOverlay } from "./defaults"
import { manageWindowState, readWindowState, resolveWindowState, windowStateFile, type WindowState } from "./window-state"
export type EarlyWindow = { id: string; win: BrowserWindow; state: WindowState; shownAt: number }
let pending: EarlyWindow | undefined
const displays = {
all: () => screen.getAllDisplays().map((display) => display.bounds),
primary: () => screen.getPrimaryDisplay().bounds,
matching: (bounds: Electron.Rectangle) => screen.getDisplayMatching(bounds).bounds,
}
// Creates and shows the first restored window the moment Electron is ready, before the rest of the
// main process has loaded. The frame options mirror windowAppearance(); the persisted background
// colour stands in for the theme until the renderer applies it, so the window is on screen while the
// bundle, the layers and the renderer boot. restoreWindows() adopts it through takeEarlyWindow().
export function createEarlyWindow() {
const ids = getStore().get(WINDOW_IDS_KEY)
const id = Array.isArray(ids) && typeof ids[0] === "string" ? ids[0] : randomUUID()
const root = path.dirname(fileURLToPath(import.meta.url))
const file = path.join(app.getPath("userData"), windowStateFile(id))
const state = resolveWindowState(readWindowState(file), { width: 1280, height: 800 }, displays)
const icons = app.isPackaged ? path.join(process.resourcesPath, "icons") : path.join(root, "../../resources/icons")
const win = new BrowserWindow({
x: state.x,
y: state.y,
width: state.width,
height: state.height,
show: true,
autoHideMenuBar: true,
title: "OpenCode",
icon: path.join(icons, `icon.${process.platform === "win32" ? "ico" : "png"}`),
backgroundColor: storedBackgroundColor(),
...(process.platform === "darwin" ? { titleBarStyle: "hidden" as const, trafficLightPosition: { x: 14, y: 14 } } : {}),
...(process.platform === "win32" ? { frame: false, titleBarStyle: "hidden" as const, titleBarOverlay: titlebarOverlay() } : {}),
webPreferences: {
preload: path.join(root, "../preload/index.cjs"),
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
additionalArguments: [windowIDArgument(id)],
},
})
manageWindowState(win, file, state, displays)
// Closing the only window before the rest of the app has adopted it is a quit.
win.once("closed", () => {
if (pending?.win !== win) return
pending = undefined
app.quit()
})
pending = { id, win, state, shownAt: Date.now() }
}
export function takeEarlyWindow() {
const taken = pending
pending = undefined
return taken
}
+38 -27
View File
@@ -24,7 +24,8 @@ import {
import { loadWindow, registerRendererProtocol } from "./protocol"
import { createWindowRegistry } from "./registry"
import { makeWindowRecovery } from "./recovery"
import { manageWindowState, readWindowState, resolveWindowState } from "./window-state"
import { takeEarlyWindow, type EarlyWindow } from "./early"
import { manageWindowState, readWindowState, resolveWindowState, windowStateFile } from "./window-state"
import { allowRendererPermissions, wireNavigationPolicy, wireRendererHeaders } from "./security"
const themeReady = new WeakMap<BrowserWindow, () => void>()
@@ -88,48 +89,62 @@ export const makeMainWindows = Effect.fn("Window.make")(function* () {
const wireWindowRecovery = yield* makeWindowRecovery
const restore = () => {
// The entry module created and showed the first restored window on ready; it is adopted here,
// before any renderer loads, so the user never waited for the layers to see a window.
const early = takeEarlyWindow()
const usable = early && !early.win.isDestroyed() ? early : undefined
const ids = registry.persisted()
return (ids.length ? ids : [randomUUID()]).map((id) => create(id))
const list = ids.length ? ids : [usable?.id ?? randomUUID()]
if (usable && !list.includes(usable.id)) usable.win.destroy()
return list.map((id) => create(id, usable?.id === id ? usable : undefined))
}
const create = (id: string = randomUUID()) => {
const create = (id: string = randomUUID(), early?: EarlyWindow) => {
const stateFile = path.join(app.getPath("userData"), windowStateFile(id))
const state = resolveWindowState(readWindowState(stateFile), { width: 1280, height: 800 }, displays)
const state = early?.state ?? resolveWindowState(readWindowState(stateFile), { width: 1280, height: 800 }, displays)
const appearance = windowAppearance(path, paths)
const win = new BrowserWindow({
x: state.x,
y: state.y,
width: state.width,
height: state.height,
show: false,
autoHideMenuBar: true,
...appearance,
webPreferences: {
...appearance.webPreferences,
additionalArguments: [windowIDArgument(id)],
},
})
const win =
early?.win ??
new BrowserWindow({
x: state.x,
y: state.y,
width: state.width,
height: state.height,
show: false,
autoHideMenuBar: true,
...appearance,
webPreferences: {
...appearance.webPreferences,
additionalArguments: [windowIDArgument(id)],
},
})
allowRendererPermissions(win)
wireWindowRecovery(win, id, () => relaunchHandler())
wireNavigationPolicy(win, (url) => runFork(openExternalURL(url)))
wireRendererHeaders(win)
manageWindowState(win, stateFile, state, displays)
if (!early) manageWindowState(win, stateFile, state, displays)
register(win, id)
wireFullscreen(win)
loadWindow(win, "index.html")
wireZoom(win)
let contentReady = false
let appliedTheme = false
let revealed = false
let revealed = !!early
const focusForTests = () => {
if (app.isPackaged || process.env.OPENCODE_TEST_ONBOARDING !== "1") return
if (process.platform === "darwin") app.focus({ steal: true })
win.focus()
}
if (early) {
focusForTests()
runFork(Effect.logInfo("main window visible", { window: id, shownAt: early.shownAt }))
}
const reveal = () => {
if (!contentReady || !appliedTheme || revealed || win.isDestroyed()) return
revealed = true
win.show()
if (!app.isPackaged && process.env.OPENCODE_TEST_ONBOARDING === "1") {
if (process.platform === "darwin") app.focus({ steal: true })
win.focus()
}
focusForTests()
runFork(Effect.logInfo("main window visible", { window: id }))
}
const ready = () => {
@@ -167,10 +182,6 @@ export const makeMainWindows = Effect.fn("Window.make")(function* () {
return { create, restore }
})
function windowStateFile(id: string) {
return `window-state-${safeWindowID(id)}.json`
}
// Mirrors windowStorage() in packages/app/src/runtime/persistence/storage.ts; it is the state
// namespace the renderer persists this window's tabs under.
function windowDataFile(id: string) {
+1 -17
View File
@@ -5,23 +5,7 @@ import { Effect, Path } from "effect"
import { scoped } from "../native/logging"
import { DesktopPaths } from "../paths"
import { documentPolicyHeader, jsCallStacksDocumentPolicy } from "./headers"
const rendererProtocol = "oc"
const rendererHost = "renderer"
protocol.registerSchemesAsPrivileged([
{
scheme: rendererProtocol,
privileges: {
secure: true,
standard: true,
supportFetchAPI: true,
stream: true,
// Let Chromium keep V8 bytecode for the renderer bundle between launches.
codeCache: true,
},
},
])
import { rendererHost, rendererProtocol } from "./scheme"
export const registerRendererProtocol = Effect.fn("Window.registerRendererProtocol")(function* () {
const path = yield* Path.Path
@@ -0,0 +1,22 @@
import { protocol } from "electron"
export const rendererProtocol = "oc"
export const rendererHost = "renderer"
// Scheme privileges can only be granted before the app is ready, so the entry module calls this
// before it loads anything else.
export function registerRendererScheme() {
protocol.registerSchemesAsPrivileged([
{
scheme: rendererProtocol,
privileges: {
secure: true,
standard: true,
supportFetchAPI: true,
stream: true,
// Let Chromium keep V8 bytecode for the renderer bundle between launches.
codeCache: true,
},
},
])
}
@@ -44,6 +44,10 @@ export function resolveWindowState(saved: unknown, defaults: { width: number; he
} satisfies WindowState
}
export function windowStateFile(id: string) {
return `window-state-${id.replace(/[^a-zA-Z0-9._-]/g, "-")}.json`
}
export function readWindowState(file: string): unknown {
if (!existsSync(file)) return undefined
try {