diff --git a/packages/desktop-electron/electron.vite.config.ts b/packages/desktop-electron/electron.vite.config.ts index 6903d5ed20..04bbf44310 100644 --- a/packages/desktop-electron/electron.vite.config.ts +++ b/packages/desktop-electron/electron.vite.config.ts @@ -33,7 +33,6 @@ export default defineConfig({ rollupOptions: { input: { main: "src/renderer/index.html", - loading: "src/renderer/loading.html", }, }, }, diff --git a/packages/desktop-electron/src/main/index.ts b/packages/desktop-electron/src/main/index.ts index 69be02428b..489e03c175 100644 --- a/packages/desktop-electron/src/main/index.ts +++ b/packages/desktop-electron/src/main/index.ts @@ -198,7 +198,8 @@ async function initialize() { setInitStep({ phase: "done" }) if (splash) { - await loadingComplete.promise + const ok = await Promise.race([loadingComplete.promise.then(() => true), delay(2_000).then(() => false)]) + if (!ok) logger.warn("loading window complete timed out") splash.close() splash = null } diff --git a/packages/desktop-electron/src/main/windows.ts b/packages/desktop-electron/src/main/windows.ts index 04ada79de3..c1654a1457 100644 --- a/packages/desktop-electron/src/main/windows.ts +++ b/packages/desktop-electron/src/main/windows.ts @@ -21,6 +21,10 @@ export function getBackgroundColor(): string | undefined { return backgroundColor } +function back(mode = tone()) { + return backgroundColor ?? (mode === "dark" ? "#101010" : "#f8f8f8") +} + function iconsDir() { return app.isPackaged ? join(process.resourcesPath, "icons") : join(root, "../../resources/icons") } @@ -69,7 +73,7 @@ export function createMainWindow(globals: Globals, opts: { show?: boolean } = {} show: opts.show ?? true, title: "OpenCode", icon: iconPath(), - backgroundColor, + backgroundColor: back(mode), ...(process.platform === "darwin" ? { titleBarStyle: "hidden" as const, @@ -98,27 +102,241 @@ export function createMainWindow(globals: Globals, opts: { show?: boolean } = {} } export function createLoadingWindow(globals: Globals) { + const mode = tone() const win = new BrowserWindow({ width: 640, height: 480, resizable: false, center: true, - show: true, + show: false, frame: false, icon: iconPath(), - backgroundColor, + backgroundColor: back(mode), webPreferences: { preload: join(root, "../preload/index.mjs"), sandbox: false, }, }) - loadWindow(win, "loading.html") + win.once("ready-to-show", () => { + if (!win.isDestroyed()) win.show() + }) + + loadSplash(win, mode) injectGlobals(win, globals) return win } +function loadSplash(win: BrowserWindow, mode: "dark" | "light") { + void win.loadURL(`data:text/html;charset=UTF-8,${encodeURIComponent(page(mode))}`) +} + +function page(mode: "dark" | "light") { + const dark = mode === "dark" + const bg = back(mode) + const base = dark ? "#7e7e7e" : "#8f8f8f" + const weak = dark ? "#343434" : "#dbdbdb" + const strong = dark ? "#ededed" : "#171717" + const track = dark ? "rgba(255,255,255,0.078)" : "rgba(0,0,0,0.051)" + const warn = dark ? "#fbb73c" : "#ebb76e" + const pulse = mark(base, strong) + const splash = mark(weak, strong) + + return ` + + + + + OpenCode + + + +
+
${pulse}
+ +
+ + +` +} + +function mark(base: string, strong: string) { + return `` +} + function loadWindow(win: BrowserWindow, html: string) { const devUrl = process.env.ELECTRON_RENDERER_URL if (devUrl) { diff --git a/packages/desktop-electron/src/renderer/html.test.ts b/packages/desktop-electron/src/renderer/html.test.ts index bd8281c2fb..e7118ba74d 100644 --- a/packages/desktop-electron/src/renderer/html.test.ts +++ b/packages/desktop-electron/src/renderer/html.test.ts @@ -16,7 +16,7 @@ const html = async (name: string) => Bun.file(join(dir, name)).text() * All local resource references must use relative paths (`./`). */ describe("electron renderer html", () => { - for (const name of ["index.html", "loading.html"]) { + for (const name of ["index.html"]) { describe(name, () => { test("script src attributes use relative paths", async () => { const content = await html(name) diff --git a/packages/desktop-electron/src/renderer/loading.html b/packages/desktop-electron/src/renderer/loading.html deleted file mode 100644 index ae3725af61..0000000000 --- a/packages/desktop-electron/src/renderer/loading.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - OpenCode - - - - - - - - - - - - -
- - - diff --git a/packages/desktop-electron/src/renderer/loading.tsx b/packages/desktop-electron/src/renderer/loading.tsx deleted file mode 100644 index acf501f378..0000000000 --- a/packages/desktop-electron/src/renderer/loading.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { MetaProvider } from "@solidjs/meta" -import { render } from "solid-js/web" -import "@opencode-ai/app/index.css" -import { Font } from "@opencode-ai/ui/font" -import { Splash } from "@opencode-ai/ui/logo" -import { Progress } from "@opencode-ai/ui/progress" -import "./styles.css" -import { createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js" -import type { InitStep, SqliteMigrationProgress } from "../preload/types" - -const root = document.getElementById("root")! -const lines = ["Just a moment...", "Migrating your database", "This may take a couple of minutes"] -const delays = [3000, 9000] - -render(() => { - const [step, setStep] = createSignal(null) - const [line, setLine] = createSignal(0) - const [percent, setPercent] = createSignal(0) - - const phase = createMemo(() => step()?.phase) - - const value = createMemo(() => { - if (phase() === "done") return 100 - return Math.max(25, Math.min(100, percent())) - }) - - window.api.awaitInitialization((next) => setStep(next as InitStep)).catch(() => undefined) - const off = window.api.onInitStep((next) => setStep(next)) - - onMount(() => { - setLine(0) - setPercent(0) - - const timers = delays.map((ms, i) => setTimeout(() => setLine(i + 1), ms)) - - const listener = window.api.onSqliteMigrationProgress((progress: SqliteMigrationProgress) => { - if (progress.type === "InProgress") setPercent(Math.max(0, Math.min(100, progress.value))) - if (progress.type === "Done") { - setPercent(100) - setStep({ phase: "done" }) - } - }) - - onCleanup(() => { - off() - listener() - timers.forEach(clearTimeout) - }) - }) - - createEffect(() => { - if (phase() !== "done") return - - const timer = setTimeout(() => window.api.loadingWindowComplete(), 1000) - onCleanup(() => clearTimeout(timer)) - }) - - const status = createMemo(() => { - if (phase() === "done") return "All done" - if (phase() === "sqlite_waiting") return lines[line()] - return "Just a moment..." - }) - - return ( - -
- - {phase() === "sqlite_waiting" ? ( -
- -
- - {status()} - - `${Math.round(value)}%`} - /> -
-
- ) : ( - - )} -
-
- ) -}, root)