perf(desktop): boot the renderer alongside the main bundle (#50108)

This commit is contained in:
Luke Parker
2026-09-20 16:15:58 +10:00
committed by GitHub
parent cdcbb0047e
commit 7fc3f68007
23 changed files with 252 additions and 109 deletions
+17 -4
View File
@@ -191,8 +191,14 @@ const phaseOrder = [
["electron js init → entry", "nodeBootstrapped", "entryStart"],
["entry → chromium ready", "entryStart", "electronReady"],
["ready → window shown", "electronReady", "windowVisible"],
["main bundle load + evaluate", "windowVisible", "bundleEvaluated"],
["layers → first log line", "bundleEvaluated", "appStarting"],
["window → renderer assets served", "windowVisible", "rendererAssetsServed"],
["main bundle load + evaluate", "rendererAssetsServed", "bundleEvaluated"],
["bundle → onboarding decided", "bundleEvaluated", "onboardingDecided"],
["onboarding → logging ready", "onboardingDecided", "loggingReady"],
["logging → first log line", "loggingReady", "appStarting"],
["first log line → storage open", "appStarting", "storageOpen"],
["storage → initialization done", "storageOpen", "initializationDone"],
["initialization → layers ready", "initializationDone", "layersReady"],
["layers → renderer process", "appStarting", "rendererProcess"],
["renderer boot → first paint", "rendererProcess", "firstPaint"],
["first paint → shell", "firstPaint", "shellVisible"],
@@ -397,8 +403,15 @@ async function launch(build: { label: string; exe: string }, run: number): Promi
nodeBootstrapped: boot && Math.round(boot.origin + boot.bootstrapComplete - spawnAt),
entryStart: main.marks.entry && main.marks.entry - spawnAt,
electronReady: main.marks.ready && main.marks.ready - spawnAt,
rendererAssetsServed: main.marks.served && main.marks.served - spawnAt,
bundleEvaluated: main.marks.bundle && main.marks.bundle - spawnAt,
onboardingDecided: main.marks.onboarding && main.marks.onboarding - spawnAt,
loggingReady: main.marks.logging && main.marks.logging - spawnAt,
crashReporterStarted: main.marks.crash && main.marks.crash - spawnAt,
appStarting: main.appStarting && main.appStarting - spawnAt,
storageOpen: main.marks.storage && main.marks.storage - spawnAt,
initializationDone: main.marks.init && main.marks.init - spawnAt,
layersReady: main.marks.layers && main.marks.layers - spawnAt,
cliVersionStart: main.versionStart && main.versionStart - spawnAt,
cliVersionDone: main.versionDone && main.versionDone - spawnAt,
serviceStarting: main.serviceStarting && main.serviceStarting - spawnAt,
@@ -593,8 +606,8 @@ function mainLog() {
// 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)
if (/app starting/.test(message))
for (const [, key, value] of entry.matchAll(/\b(entry|ready|window|bundle): (\d{10,})/g)) marks[key] = Number(value)
if (/app starting|layers ready/.test(message))
for (const [, key, value] of entry.matchAll(/\b(\w+): (\d{10,})/g)) marks[key] = Number(value)
timeline.push([new Date(m[1].replace(" ", "T")).getTime(), name.replace(/\.log$/, ""), message])
}
}
@@ -0,0 +1,10 @@
param([Parameter(Mandatory)][string]$Name, [int]$Runs = 5)
$ErrorActionPreference = "Stop"
Set-Location $PSScriptRoot\..
# electron-vite directly: the package's prebuild hook re-downloads the CLI, which the bench keeps fixed.
bunx electron-vite build 2>&1 | Select-String -Pattern "built in|error" | Select-Object -Last 3
if (-not (Test-Path out\main\index.js)) { throw "build failed" }
bunx electron-builder --win --dir --config electron-builder.config.ts 2>&1 | Select-String -Pattern "error|signing with signtool.*OpenCode Dev" | Select-Object -Last 2
if (Test-Path "dist\$Name-unpacked") { Remove-Item "dist\$Name-unpacked" -Recurse -Force }
Rename-Item -Path dist\win-unpacked -NewName "$Name-unpacked"
bun ./scripts/bench-startup.ts --exe "dist\base-unpacked\OpenCode Dev.exe" --compare "dist\$Name-unpacked\OpenCode Dev.exe" --runs $Runs --warmup 1 --window-at=-1700,20 --out "dist\bench-startup\$Name" 2>&1 | Select-String -Pattern "^warm service|^\s{2,}|^phases|^\S+\s+\d+\s+\(|^report|^warm-up|Error|error" | Select-Object -Last 45
+4 -1
View File
@@ -14,7 +14,9 @@ import { marks } from "./lifecycle/marks"
marks.bundle = Date.now()
const runIpc = Effect.fn("Desktop.runIpc")(function* () {
const lifecycle = yield* ApplicationLifecycle.Service
const lifecycle = yield* ApplicationLifecycle.Service
marks.layers = Date.now()
yield* Effect.logInfo("layers ready", { marks })
const ipc = yield* Ipc.registerIpcHandlers
if (lifecycle.restoreWindows().length) ipc.installMenu()
yield* installContextMenu
@@ -36,3 +38,4 @@ runIpc().pipe(
Effect.scoped,
NodeRuntime.runMain,
)
@@ -4,7 +4,7 @@ import { EventRpcs } from "../../shared/ipc-rpc"
import { ipcEventStream } from "../ipc-events"
import { IpcPortHandoff } from "../ipc-transport"
import { Shutdown } from "../lifecycle/shutdown"
import { isRendererUrl } from "../windows/protocol"
import { isRendererUrl } from "../windows/scheme"
import { DesktopStorage } from "../storage"
import { sender } from "./context"
+5 -2
View File
@@ -65,12 +65,15 @@ export const registerIpcHandlers = Effect.gen(function* () {
if (input.type !== "keyDown" || input.key !== "Escape") return
win.webContents.send(DragCancelEvent)
})
win.webContents.on("did-finish-load", () => {
const post = () => {
if (win.isDestroyed() || win.webContents.isDestroyed()) return
const channel = new MessageChannelMain()
handoff.bind(win.webContents, channel.port1)
win.webContents.postMessage(IpcTransportPort, null, [channel.port2])
})
}
win.webContents.on("did-finish-load", post)
// The first window starts loading before the layers exist and may already be done.
if (!win.webContents.isLoading() && win.webContents.getURL()) post()
}
yield* Effect.sync(() => {
app.on("browser-window-created", wire)
@@ -4,7 +4,8 @@ import { app } from "electron"
import { Effect, Path } from "effect"
import { DesktopPaths } from "../paths"
import { getUserShell, loadShellEnv } from "../service/shell-env"
import { registerRendererProtocol, setDockIcon } from "../windows"
import { registerRendererProtocol, setDockIcon, setProtocolReporter } from "../windows"
import { scoped } from "../native/logging"
// 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.
@@ -37,7 +38,11 @@ export const prepareDesktop = Effect.gen(function* () {
const paths = yield* DesktopPaths.resolve
if (app.isPackaged || process.env.OPENCODE_DESKTOP_DISABLE_PROTOCOL_REGISTRATION !== "1")
app.setAsDefaultProtocolClient("opencode")
yield* registerRendererProtocol()
const runFork = Effect.runForkWith(yield* Effect.context())
setProtocolReporter((level, message, data) =>
runFork(scoped("protocol", level === "error" ? Effect.logError(message, data) : Effect.logWarning(message, data))),
)
registerRendererProtocol(paths.rendererRoot)
setDockIcon(path, paths)
})
@@ -9,6 +9,7 @@ import { DesktopLogging, scoped } from "../native/logging"
import { DesktopStorage } from "../storage"
import { safeWebContentsURL } from "../windows/state"
import { getLastFocusedWindow, makeMainWindows, setAppQuitting, setRelaunchHandler } from "../windows"
import { marks } from "./marks"
import { initializeFirstLaunchOnboarding } from "./onboarding"
import { Shutdown } from "./shutdown"
@@ -157,6 +158,7 @@ export const layer = Layer.unwrap(
// 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"))
marks.onboarding = Date.now()
return runtime.pipe(Layer.provideMerge(platform))
}),
)
+6 -3
View File
@@ -1,4 +1,7 @@
// Startup marks, epoch ms. The entry module records them before any logger exists; the logging
// layer reports them with "app starting" so the startup benchmark can split the time before the
// first log line into Electron's own initialisation, our entry, and the main bundle.
export const marks: { entry: number; ready?: number; window?: number; bundle?: number } = { entry: Date.now() }
// layer reports the early ones with "app starting" and the rest with "layers ready", so the startup
// benchmark can split the time before the renderer gets its IPC port into Electron's own
// initialisation, our entry, the main bundle and each layer.
export const marks: { entry: number } & Partial<
Record<"ready" | "window" | "served" | "bundle" | "onboarding" | "logging" | "crash" | "storage" | "init" | "layers", number>
> = { entry: Date.now() }
@@ -2,6 +2,7 @@ export * as DesktopStorage from "./index"
import { app, BrowserWindow } from "electron"
import { Context, Effect, Layer, Path } from "effect"
import { marks } from "../lifecycle/marks"
import { openDatabase } from "./database"
import { createDraftStore } from "./drafts"
import { importLegacyStores } from "./legacy"
@@ -40,6 +41,7 @@ export const layer = Layer.effect(
storage.close()
}),
)
marks.storage = Date.now()
return Service.of(storage)
}),
)
@@ -0,0 +1,20 @@
import { windowBootstrapArgument, type WindowBootstrap } from "../../shared/window-bootstrap"
import { getDefaultServerUrl } from "../service/server-settings"
import { FIRST_LAUNCH_ONBOARDING_COMPLETE_KEY } from "../storage/keys"
import { getStore } from "../storage/store"
// The settings store is already in memory when a window is created, so the renderer gets the
// answers its shell gate would otherwise ask for over IPC. A fresh install has no onboarding
// decision yet; the renderer asks once the layers have made one.
export function windowBootstrap(id: string): WindowBootstrap {
const complete = getStore().get(FIRST_LAUNCH_ONBOARDING_COMPLETE_KEY)
return {
id,
firstLaunchPending: typeof complete === "boolean" ? !complete : undefined,
defaultServerUrl: getDefaultServerUrl(),
}
}
export function windowArguments(id: string) {
return [windowBootstrapArgument(windowBootstrap(id))]
}
+33 -5
View File
@@ -1,14 +1,25 @@
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 { app, BrowserWindow, screen, shell } from "electron"
import { resolveExternalURL } from "../files/external-url"
import { windowArguments } from "./bootstrap"
import { WINDOW_IDS_KEY } from "../storage/keys"
import { getStore } from "../storage/store"
import { storedBackgroundColor, titlebarOverlay } from "./defaults"
import { registerRendererProtocol } from "./protocol"
import { loadWindow } from "./scheme"
import { allowRendererPermissions, wireNavigationPolicy, wireRendererHeaders } from "./security"
import { manageWindowState, readWindowState, resolveWindowState, windowStateFile, type WindowState } from "./window-state"
export type EarlyWindow = { id: string; win: BrowserWindow; state: WindowState; shownAt: number }
export type EarlyWindow = {
id: string
win: BrowserWindow
state: WindowState
shownAt: number
// Navigation policy is wired before the layers exist; the adopter swaps in the logged version.
openExternal: (url: string) => void
}
let pending: EarlyWindow | undefined
@@ -46,7 +57,7 @@ export function createEarlyWindow() {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
additionalArguments: [windowIDArgument(id)],
additionalArguments: windowArguments(id),
},
})
manageWindowState(win, file, state, displays)
@@ -56,7 +67,24 @@ export function createEarlyWindow() {
pending = undefined
app.quit()
})
pending = { id, win, state, shownAt: Date.now() }
const record: EarlyWindow = {
id,
win,
state,
shownAt: Date.now(),
openExternal: (url) => {
const target = resolveExternalURL(url)
if (target) void shell.openExternal(target)
},
}
pending = record
// The renderer boots while the main bundle and layers load, instead of after them. Everything the
// page needs before its first request is wired here; the IPC port arrives once the layers are up.
registerRendererProtocol(path.join(root, "../renderer"))
allowRendererPermissions(win)
wireNavigationPolicy(win, (url) => record.openExternal(url))
wireRendererHeaders(win)
loadWindow(win, "index.html")
}
export function takeEarlyWindow() {
+15 -8
View File
@@ -7,7 +7,7 @@ import { DesktopPaths } from "../paths"
import { DesktopStorage } from "../storage"
import { getStore } from "../storage/store"
import { WINDOW_IDS_KEY } from "../storage/keys"
import { windowIDArgument } from "../../shared/window-bootstrap"
import { windowArguments } from "./bootstrap"
import {
getBackgroundColor,
getPinchZoomEnabled,
@@ -21,7 +21,8 @@ import {
wireFullscreen,
wireZoom,
} from "./appearance"
import { loadWindow, registerRendererProtocol } from "./protocol"
import { registerRendererProtocol, setProtocolReporter } from "./protocol"
import { loadWindow } from "./scheme"
import { createWindowRegistry } from "./registry"
import { makeWindowRecovery } from "./recovery"
import { takeEarlyWindow, type EarlyWindow } from "./early"
@@ -48,6 +49,7 @@ export {
getBackgroundColor,
getPinchZoomEnabled,
registerRendererProtocol,
setProtocolReporter,
setBackgroundColor,
setDockIcon,
setPinchZoomEnabled,
@@ -115,18 +117,23 @@ export const makeMainWindows = Effect.fn("Window.make")(function* () {
...appearance,
webPreferences: {
...appearance.webPreferences,
additionalArguments: [windowIDArgument(id)],
additionalArguments: windowArguments(id),
},
})
allowRendererPermissions(win)
// The early window was secured and loaded when it was created; only its external-URL policy is
// upgraded to the logged one.
if (early) early.openExternal = (url) => runFork(openExternalURL(url))
if (!early) {
allowRendererPermissions(win)
wireNavigationPolicy(win, (url) => runFork(openExternalURL(url)))
wireRendererHeaders(win)
manageWindowState(win, stateFile, state, displays)
}
wireWindowRecovery(win, id, () => relaunchHandler())
wireNavigationPolicy(win, (url) => runFork(openExternalURL(url)))
wireRendererHeaders(win)
if (!early) manageWindowState(win, stateFile, state, displays)
register(win, id)
wireFullscreen(win)
loadWindow(win, "index.html")
if (!early) loadWindow(win, "index.html")
wireZoom(win)
let contentReady = false
let appliedTheme = false
+44 -59
View File
@@ -1,73 +1,57 @@
import { net, protocol } from "electron"
import type { BrowserWindow } from "electron"
import path from "node:path"
import { pathToFileURL } from "node:url"
import { Effect, Path } from "effect"
import { scoped } from "../native/logging"
import { DesktopPaths } from "../paths"
import { documentPolicyHeader, jsCallStacksDocumentPolicy } from "./headers"
import { rendererHost, rendererProtocol } from "./scheme"
export const registerRendererProtocol = Effect.fn("Window.registerRendererProtocol")(function* () {
const path = yield* Path.Path
const paths = yield* DesktopPaths.resolve
const runFork = Effect.runForkWith(yield* Effect.context<never>())
export type ProtocolReport = (level: "warning" | "error", message: string, data: Record<string, unknown>) => void
// The entry module registers the handler the moment the first window exists, before logging is up,
// so problems go to the console until the logging layer installs a reporter.
let report: ProtocolReport = (level, message, data) => console[level === "error" ? "error" : "warn"](message, data)
export function setProtocolReporter(reporter: ProtocolReport) {
report = reporter
}
export function registerRendererProtocol(rendererRoot: string) {
if (protocol.isProtocolHandled(rendererProtocol)) return
protocol.handle(rendererProtocol, async (request) => {
const url = new URL(request.url)
if (url.host !== rendererHost) {
runFork(scoped("protocol", Effect.logWarning("rejected host", { url: request.url })))
return new Response("Not found", { status: 404 })
}
const file = path.resolve(paths.rendererRoot, `.${decodeURIComponent(url.pathname)}`)
const rel = path.relative(paths.rendererRoot, file)
if (rel.startsWith("..") || path.isAbsolute(rel)) {
runFork(scoped("protocol", Effect.logWarning("rejected path", { url: request.url, file })))
return new Response("Not found", { status: 404 })
}
try {
const range = request.headers.get("range")
const response = await net.fetch(pathToFileURL(file).toString(), { headers: range ? { range } : undefined })
if (response.status >= 400) {
runFork(
scoped(
"protocol",
Effect.logError("fetch failed", {
url: request.url,
file,
status: response.status,
statusText: response.statusText,
}),
),
)
}
return addDocumentPolicy(response, file)
} catch (error) {
runFork(scoped("protocol", Effect.logError("fetch error", { url: request.url, file, error })))
return new Response("Not found", { status: 404 })
}
return serve(request, rendererRoot)
})
})
export function loadWindow(win: BrowserWindow, html: string) {
const devUrl = process.env.ELECTRON_RENDERER_URL
if (devUrl) {
void win.loadURL(new URL(html, devUrl).toString())
return
}
void win.loadURL(`${rendererProtocol}://${rendererHost}/${html}`)
}
export function isRendererUrl(value?: string, html = false) {
if (!value || !URL.canParse(value)) return false
const url = new URL(value)
if (html && !url.pathname.endsWith(".html")) return false
if (url.protocol === `${rendererProtocol}:` && url.host === rendererHost) return true
const devUrl = process.env.ELECTRON_RENDERER_URL
if (!devUrl || !URL.canParse(devUrl)) return false
return url.origin === new URL(devUrl).origin
async function serve(request: Request, rendererRoot: string) {
const url = new URL(request.url)
if (url.host !== rendererHost) {
report("warning", "rejected host", { url: request.url })
return new Response("Not found", { status: 404 })
}
const file = path.resolve(rendererRoot, `.${decodeURIComponent(url.pathname)}`)
const rel = path.relative(rendererRoot, file)
if (rel.startsWith("..") || path.isAbsolute(rel)) {
report("warning", "rejected path", { url: request.url, file })
return new Response("Not found", { status: 404 })
}
try {
const range = request.headers.get("range")
const response = await net.fetch(pathToFileURL(file).toString(), { headers: range ? { range } : undefined })
if (response.status >= 400) {
report("error", "fetch failed", {
url: request.url,
file,
status: response.status,
statusText: response.statusText,
})
}
return addDocumentPolicy(response, file)
} catch (error) {
report("error", "fetch error", { url: request.url, file, error })
return new Response("Not found", { status: 404 })
}
}
function addDocumentPolicy(response: Response, file: string) {
@@ -76,3 +60,4 @@ function addDocumentPolicy(response: Response, file: string) {
headers.set(documentPolicyHeader, jsCallStacksDocumentPolicy)
return new Response(response.body, { status: response.status, statusText: response.statusText, headers })
}
@@ -1,8 +1,28 @@
import { protocol } from "electron"
import type { BrowserWindow } from "electron"
export const rendererProtocol = "oc"
export const rendererHost = "renderer"
export function loadWindow(win: BrowserWindow, html: string) {
const devUrl = process.env.ELECTRON_RENDERER_URL
if (devUrl) {
void win.loadURL(new URL(html, devUrl).toString())
return
}
void win.loadURL(`${rendererProtocol}://${rendererHost}/${html}`)
}
export function isRendererUrl(value?: string, html = false) {
if (!value || !URL.canParse(value)) return false
const url = new URL(value)
if (html && !url.pathname.endsWith(".html")) return false
if (url.protocol === `${rendererProtocol}:` && url.host === rendererHost) return true
const devUrl = process.env.ELECTRON_RENDERER_URL
if (!devUrl || !URL.canParse(devUrl)) return false
return url.origin === new URL(devUrl).origin
}
// 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() {
@@ -1,7 +1,7 @@
import type { BrowserWindow } from "electron"
import { SidecarCredentials } from "../service/sidecar-credentials"
import { addRendererHeaders, hasHeader, upsertHeader } from "./headers"
import { isRendererUrl } from "./protocol"
import { isRendererUrl } from "./scheme"
const rendererPermissions = new Set(["clipboard-sanitized-write", "notifications"])
+5 -2
View File
@@ -1,6 +1,6 @@
import { contextBridge, ipcRenderer, webUtils } from "electron"
import { DragCancelEvent, IpcTransportPort } from "../shared/ipc-transport"
import { windowIDFromArguments } from "../shared/window-bootstrap"
import { windowBootstrapFromArguments } from "../shared/window-bootstrap"
ipcRenderer.on(IpcTransportPort, (event) => {
const port = event.ports[0]
@@ -9,7 +9,10 @@ ipcRenderer.on(IpcTransportPort, (event) => {
ipcRenderer.on(DragCancelEvent, () => window.dispatchEvent(new Event(DragCancelEvent)))
const bootstrap = windowBootstrapFromArguments(process.argv)
contextBridge.exposeInMainWorld("electron", {
windowID: windowIDFromArguments(process.argv),
windowID: bootstrap.id,
bootstrap,
getPathForFile: (file: File) => webUtils.getPathForFile(file),
})
+3
View File
@@ -1,4 +1,7 @@
import type { WindowBootstrap } from "../shared/window-bootstrap"
export type ElectronNative = {
windowID: string
bootstrap: WindowBootstrap
getPathForFile(file: File): string
}
@@ -5,6 +5,7 @@ import type { UpdaterState } from "@opencode/app/updater"
import type { WslServersPlatform } from "@opencode/app/wsl/types"
import type { SshPlatform } from "@opencode/app/ssh"
import type { BrowserPaneRequest } from "../shared/ipc-rpc/browser"
import type { WindowBootstrap } from "../shared/window-bootstrap"
import type {
ClipboardImage,
DirectoryPickerOptions,
@@ -53,6 +54,7 @@ export type ElectronAPI = {
draftBlobPut(data: ArrayBuffer): Promise<string>
draftBlobGet(id: string): Promise<ArrayBuffer | null>
getWindowID(): string
getWindowBootstrap(): WindowBootstrap
themeReady(): Promise<void>
onMenuCommand(cb: (id: string) => void): () => void
onDeepLink(cb: (urls: string[]) => void): () => void
+1
View File
@@ -111,6 +111,7 @@ export const api: ElectronAPI = {
draftBlobGet: (id) => invoke("DraftsGetBlob", { id }).then((data) => (data ? toArrayBuffer(data) : null)),
getWindowID: () => window.electron.windowID,
getWindowBootstrap: () => window.electron.bootstrap,
themeReady: () => invoke("WindowThemeReady"),
onMenuCommand: (cb) => listen("MenuCommandTriggered", (event) => cb(event.id)),
onDeepLink: (cb) => listen("DeepLinksOpened", (event) => cb(mutable(event.urls))),
+13 -5
View File
@@ -48,15 +48,23 @@ export function DesktopApp(props: { api: ElectronAPI; updater: UpdaterPlatform;
drawingReady: false,
route,
})
// The window was created with the answers the shell gate needs; only a fresh install, which has no
// onboarding decision yet, asks over IPC and waits for the port.
const bootstrap = props.api.getWindowBootstrap()
const [firstLaunch] = createResource(() =>
props.api.isFirstLaunchOnboardingPending().catch((error) => {
console.error("[desktop-onboarding] first launch check failed", error)
return false
}),
bootstrap.firstLaunchPending !== undefined
? Promise.resolve(bootstrap.firstLaunchPending)
: props.api.isFirstLaunchOnboardingPending().catch((error) => {
console.error("[desktop-onboarding] first launch check failed", error)
return false
}),
)
const platform = createDesktopPlatform(props.api, windowState, props.updater)
const [sidecar, { mutate: setSidecar }] = createResource(() => props.api.awaitInitialization())
const [defaultServer] = createResource(() => platform.getDefaultServer?.())
const [defaultServer] = createResource(async () => {
if (bootstrap.defaultServerUrl === undefined) return platform.getDefaultServer?.()
return bootstrap.defaultServerUrl ? ServerConnection.Key.make(bootstrap.defaultServerUrl) : null
})
const [locale] = createResource(() => preloadStoredLocale(platform))
const [initialRoute] = createResource(
() => !firstLaunch.loading && (firstLaunch() && initialUrl === "/" ? "/new-session" : initialUrl),
@@ -1,9 +1,22 @@
import { loadLocaleDict, normalizeLocale, type Locale, type Platform } from "@opencode/app/desktop"
import { storedLocaleValue } from "./locale-value"
// The stored language lives in the main process's SQLite store, behind the IPC port. A copy of the
// last answer in localStorage lets the shell mount without waiting for the port; the store is still
// asked every launch and the copy refreshed, and the language provider hydrates from the store
// itself, so a stale copy costs one visible switch, not a wrong language.
const cacheKey = "opencode.desktop.language"
export async function preloadStoredLocale(platform: Platform) {
const raw = await platform.storage?.("opencode.global.dat").getItem("language")
const locale = storedLocale(raw)
const fresh = Promise.resolve(platform.storage?.("opencode.global.dat").getItem("language")).then(
(raw) => {
localStorage.setItem(cacheKey, raw ?? "")
return raw
},
() => undefined,
)
const cached = localStorage.getItem(cacheKey)
const locale = storedLocale(cached ?? (await fresh))
if (!locale) return
if (locale !== "en") await loadLocaleDict(locale)
return locale
@@ -1,13 +1,17 @@
import { describe, expect, test } from "bun:test"
import { windowIDArgument, windowIDFromArguments } from "./window-bootstrap"
import { windowBootstrapArgument, windowBootstrapFromArguments } from "./window-bootstrap"
describe("window bootstrap", () => {
test("round-trips the window ID through renderer arguments", () => {
const id = "window/id with spaces"
expect(windowIDFromArguments(["electron", windowIDArgument(id)])).toBe(id)
test("round-trips through argv", () => {
const bootstrap = { id: "win a/b ü", firstLaunchPending: false, defaultServerUrl: "http://127.0.0.1:1234" }
expect(windowBootstrapFromArguments(["electron", windowBootstrapArgument(bootstrap)])).toEqual(bootstrap)
})
test("requires a window ID argument", () => {
expect(() => windowIDFromArguments(["electron"])).toThrow("Window ID argument not found")
test("keeps unknown values absent", () => {
expect(windowBootstrapFromArguments([windowBootstrapArgument({ id: "x" })])).toEqual({ id: "x" })
})
test("throws when the argument is missing", () => {
expect(() => windowBootstrapFromArguments(["electron"])).toThrow("Window bootstrap argument not found")
})
})
@@ -1,11 +1,19 @@
const windowIDPrefix = "--opencode-window-id="
export function windowIDArgument(id: string) {
return windowIDPrefix + encodeURIComponent(id)
// What the main process already knows when it creates a window, handed to the renderer through the
// preload's argv so the shell can mount before the IPC port exists. Undefined means "ask over IPC".
export type WindowBootstrap = {
id: string
firstLaunchPending?: boolean
defaultServerUrl?: string | null
}
export function windowIDFromArguments(args: readonly string[]) {
const value = args.find((arg) => arg.startsWith(windowIDPrefix))?.slice(windowIDPrefix.length)
if (!value) throw new Error("Window ID argument not found")
return decodeURIComponent(value)
const prefix = "--opencode-window="
export function windowBootstrapArgument(bootstrap: WindowBootstrap) {
return prefix + encodeURIComponent(JSON.stringify(bootstrap))
}
export function windowBootstrapFromArguments(args: readonly string[]): WindowBootstrap {
const value = args.find((arg) => arg.startsWith(prefix))?.slice(prefix.length)
if (!value) throw new Error("Window bootstrap argument not found")
return JSON.parse(decodeURIComponent(value))
}