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
+
+
+
+
+
+
+`
+}
+
+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()}
-
-
-
- ) : (
-
- )}
-
-
- )
-}, root)