mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-10 11:39:45 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 943d82ce0d | |||
| 10a43e0f6a |
@@ -13,6 +13,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
|
|||||||
import { type Duration, Effect } from "effect"
|
import { type Duration, Effect } from "effect"
|
||||||
import {
|
import {
|
||||||
type Component,
|
type Component,
|
||||||
|
createEffect,
|
||||||
createMemo,
|
createMemo,
|
||||||
createResource,
|
createResource,
|
||||||
createSignal,
|
createSignal,
|
||||||
@@ -161,7 +162,7 @@ const effectMinDuration =
|
|||||||
<A, E, R>(e: Effect.Effect<A, E, R>) =>
|
<A, E, R>(e: Effect.Effect<A, E, R>) =>
|
||||||
Effect.all([e, Effect.sleep(duration)], { concurrency: "unbounded" }).pipe(Effect.map((v) => v[0]))
|
Effect.all([e, Effect.sleep(duration)], { concurrency: "unbounded" }).pipe(Effect.map((v) => v[0]))
|
||||||
|
|
||||||
function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
|
function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean; onReady?: () => void }>) {
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const checkServerHealth = useCheckServerHealth()
|
const checkServerHealth = useCheckServerHealth()
|
||||||
|
|
||||||
@@ -189,6 +190,16 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let sent = false
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (sent) return
|
||||||
|
const ready = checkMode() === "blocking" ? !startupHealthCheck.loading : startupHealthCheck.state !== "pending"
|
||||||
|
if (!ready) return
|
||||||
|
sent = true
|
||||||
|
props.onReady?.()
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show
|
<Show
|
||||||
when={checkMode() === "blocking" ? !startupHealthCheck.loading : startupHealthCheck.state !== "pending"}
|
when={checkMode() === "blocking" ? !startupHealthCheck.loading : startupHealthCheck.state !== "pending"}
|
||||||
@@ -281,6 +292,7 @@ export function AppInterface(props: {
|
|||||||
servers?: Array<ServerConnection.Any>
|
servers?: Array<ServerConnection.Any>
|
||||||
router?: Component<BaseRouterProps>
|
router?: Component<BaseRouterProps>
|
||||||
disableHealthCheck?: boolean
|
disableHealthCheck?: boolean
|
||||||
|
onReady?: () => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ServerProvider
|
<ServerProvider
|
||||||
@@ -288,7 +300,7 @@ export function AppInterface(props: {
|
|||||||
disableHealthCheck={props.disableHealthCheck}
|
disableHealthCheck={props.disableHealthCheck}
|
||||||
servers={props.servers}
|
servers={props.servers}
|
||||||
>
|
>
|
||||||
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
<ConnectionGate disableHealthCheck={props.disableHealthCheck} onReady={props.onReady}>
|
||||||
<ServerKey>
|
<ServerKey>
|
||||||
<GlobalSDKProvider>
|
<GlobalSDKProvider>
|
||||||
<GlobalSyncProvider>
|
<GlobalSyncProvider>
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export default defineConfig({
|
|||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
main: "src/renderer/index.html",
|
main: "src/renderer/index.html",
|
||||||
loading: "src/renderer/loading.html",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -41,8 +41,11 @@ const initEmitter = new EventEmitter()
|
|||||||
let initStep: InitStep = { phase: "server_waiting" }
|
let initStep: InitStep = { phase: "server_waiting" }
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null
|
let mainWindow: BrowserWindow | null = null
|
||||||
|
let splash: BrowserWindow | null = null
|
||||||
|
let ready = false
|
||||||
let sidecar: CommandChild | null = null
|
let sidecar: CommandChild | null = null
|
||||||
const loadingComplete = defer<void>()
|
const loadingComplete = defer<void>()
|
||||||
|
const mainReady = defer<void>()
|
||||||
|
|
||||||
const pendingDeepLinks: string[] = []
|
const pendingDeepLinks: string[] = []
|
||||||
|
|
||||||
@@ -112,6 +115,11 @@ function emitDeepLinks(urls: string[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function focusMainWindow() {
|
function focusMainWindow() {
|
||||||
|
if (!ready) {
|
||||||
|
splash?.show()
|
||||||
|
splash?.focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!mainWindow) return
|
if (!mainWindow) return
|
||||||
mainWindow.show()
|
mainWindow.show()
|
||||||
mainWindow.focus()
|
mainWindow.focus()
|
||||||
@@ -121,12 +129,14 @@ function setInitStep(step: InitStep) {
|
|||||||
initStep = step
|
initStep = step
|
||||||
logger.log("init step", { step })
|
logger.log("init step", { step })
|
||||||
initEmitter.emit("step", step)
|
initEmitter.emit("step", step)
|
||||||
|
BrowserWindow.getAllWindows().forEach((win) => {
|
||||||
|
win.webContents.send("init-step", step)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initialize() {
|
async function initialize() {
|
||||||
const needsMigration = !sqliteFileExists()
|
const needsMigration = !sqliteFileExists()
|
||||||
const sqliteDone = needsMigration ? defer<void>() : undefined
|
const sqliteDone = needsMigration ? defer<void>() : undefined
|
||||||
let overlay: BrowserWindow | null = null
|
|
||||||
|
|
||||||
const port = await getSidecarPort()
|
const port = await getSidecarPort()
|
||||||
const hostname = "127.0.0.1"
|
const hostname = "127.0.0.1"
|
||||||
@@ -147,7 +157,7 @@ async function initialize() {
|
|||||||
|
|
||||||
events.on("sqlite", (progress: SqliteMigrationProgress) => {
|
events.on("sqlite", (progress: SqliteMigrationProgress) => {
|
||||||
setInitStep({ phase: "sqlite_waiting" })
|
setInitStep({ phase: "sqlite_waiting" })
|
||||||
if (overlay) sendSqliteMigrationProgress(overlay, progress)
|
if (splash) sendSqliteMigrationProgress(splash, progress)
|
||||||
if (mainWindow) sendSqliteMigrationProgress(mainWindow, progress)
|
if (mainWindow) sendSqliteMigrationProgress(mainWindow, progress)
|
||||||
if (progress.type === "Done") sqliteDone?.resolve()
|
if (progress.type === "Done") sqliteDone?.resolve()
|
||||||
})
|
})
|
||||||
@@ -173,25 +183,31 @@ async function initialize() {
|
|||||||
deepLinks: pendingDeepLinks,
|
deepLinks: pendingDeepLinks,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needsMigration) {
|
const startup = (async () => {
|
||||||
const show = await Promise.race([loadingTask.then(() => false), delay(1_000).then(() => true)])
|
await loadingTask
|
||||||
if (show) {
|
setInitStep({ phase: "app_waiting" })
|
||||||
overlay = createLoadingWindow(globals)
|
mainWindow = createMainWindow(globals, { show: false })
|
||||||
await delay(1_000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await loadingTask
|
const ok = await Promise.race([mainReady.promise.then(() => true), delay(15_000).then(() => false)])
|
||||||
|
if (!ok) logger.warn("main window ready timed out")
|
||||||
|
})()
|
||||||
|
|
||||||
|
splash = createLoadingWindow(globals)
|
||||||
|
|
||||||
|
await startup
|
||||||
setInitStep({ phase: "done" })
|
setInitStep({ phase: "done" })
|
||||||
|
|
||||||
if (overlay) {
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
mainWindow = createMainWindow(globals)
|
|
||||||
wireMenu()
|
wireMenu()
|
||||||
|
ready = true
|
||||||
overlay?.close()
|
mainWindow?.show()
|
||||||
|
mainWindow?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
function wireMenu() {
|
function wireMenu() {
|
||||||
@@ -240,6 +256,7 @@ registerIpcHandlers({
|
|||||||
wslPath: async (path, mode) => wslPath(path, mode),
|
wslPath: async (path, mode) => wslPath(path, mode),
|
||||||
resolveAppPath: async (appName) => resolveAppPath(appName),
|
resolveAppPath: async (appName) => resolveAppPath(appName),
|
||||||
loadingWindowComplete: () => loadingComplete.resolve(),
|
loadingWindowComplete: () => loadingComplete.resolve(),
|
||||||
|
mainWindowReady: () => mainReady.resolve(),
|
||||||
runUpdater: async (alertOnFail) => checkForUpdates(alertOnFail),
|
runUpdater: async (alertOnFail) => checkForUpdates(alertOnFail),
|
||||||
checkUpdate: async () => checkUpdate(),
|
checkUpdate: async () => checkUpdate(),
|
||||||
installUpdate: async () => installUpdate(),
|
installUpdate: async () => installUpdate(),
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type Deps = {
|
|||||||
wslPath: (path: string, mode: "windows" | "linux" | null) => Promise<string>
|
wslPath: (path: string, mode: "windows" | "linux" | null) => Promise<string>
|
||||||
resolveAppPath: (appName: string) => Promise<string | null>
|
resolveAppPath: (appName: string) => Promise<string | null>
|
||||||
loadingWindowComplete: () => void
|
loadingWindowComplete: () => void
|
||||||
|
mainWindowReady: () => void
|
||||||
runUpdater: (alertOnFail: boolean) => Promise<void> | void
|
runUpdater: (alertOnFail: boolean) => Promise<void> | void
|
||||||
checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>
|
checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>
|
||||||
installUpdate: () => Promise<void> | void
|
installUpdate: () => Promise<void> | void
|
||||||
@@ -56,6 +57,7 @@ export function registerIpcHandlers(deps: Deps) {
|
|||||||
)
|
)
|
||||||
ipcMain.handle("resolve-app-path", (_event: IpcMainInvokeEvent, appName: string) => deps.resolveAppPath(appName))
|
ipcMain.handle("resolve-app-path", (_event: IpcMainInvokeEvent, appName: string) => deps.resolveAppPath(appName))
|
||||||
ipcMain.on("loading-window-complete", () => deps.loadingWindowComplete())
|
ipcMain.on("loading-window-complete", () => deps.loadingWindowComplete())
|
||||||
|
ipcMain.on("main-window-ready", () => deps.mainWindowReady())
|
||||||
ipcMain.handle("run-updater", (_event: IpcMainInvokeEvent, alertOnFail: boolean) => deps.runUpdater(alertOnFail))
|
ipcMain.handle("run-updater", (_event: IpcMainInvokeEvent, alertOnFail: boolean) => deps.runUpdater(alertOnFail))
|
||||||
ipcMain.handle("check-update", () => deps.checkUpdate())
|
ipcMain.handle("check-update", () => deps.checkUpdate())
|
||||||
ipcMain.handle("install-update", () => deps.installUpdate())
|
ipcMain.handle("install-update", () => deps.installUpdate())
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ export function getBackgroundColor(): string | undefined {
|
|||||||
return backgroundColor
|
return backgroundColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function back(mode = tone()) {
|
||||||
|
return backgroundColor ?? (mode === "dark" ? "#101010" : "#f8f8f8")
|
||||||
|
}
|
||||||
|
|
||||||
function iconsDir() {
|
function iconsDir() {
|
||||||
return app.isPackaged ? join(process.resourcesPath, "icons") : join(root, "../../resources/icons")
|
return app.isPackaged ? join(process.resourcesPath, "icons") : join(root, "../../resources/icons")
|
||||||
}
|
}
|
||||||
@@ -54,7 +58,7 @@ export function setDockIcon() {
|
|||||||
if (!icon.isEmpty()) app.dock?.setIcon(icon)
|
if (!icon.isEmpty()) app.dock?.setIcon(icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMainWindow(globals: Globals) {
|
export function createMainWindow(globals: Globals, opts: { show?: boolean } = {}) {
|
||||||
const state = windowState({
|
const state = windowState({
|
||||||
defaultWidth: 1280,
|
defaultWidth: 1280,
|
||||||
defaultHeight: 800,
|
defaultHeight: 800,
|
||||||
@@ -66,10 +70,10 @@ export function createMainWindow(globals: Globals) {
|
|||||||
y: state.y,
|
y: state.y,
|
||||||
width: state.width,
|
width: state.width,
|
||||||
height: state.height,
|
height: state.height,
|
||||||
show: true,
|
show: opts.show ?? true,
|
||||||
title: "OpenCode",
|
title: "OpenCode",
|
||||||
icon: iconPath(),
|
icon: iconPath(),
|
||||||
backgroundColor,
|
backgroundColor: back(mode),
|
||||||
...(process.platform === "darwin"
|
...(process.platform === "darwin"
|
||||||
? {
|
? {
|
||||||
titleBarStyle: "hidden" as const,
|
titleBarStyle: "hidden" as const,
|
||||||
@@ -104,29 +108,235 @@ export function createLoadingWindow(globals: Globals) {
|
|||||||
height: 480,
|
height: 480,
|
||||||
resizable: false,
|
resizable: false,
|
||||||
center: true,
|
center: true,
|
||||||
show: true,
|
show: false,
|
||||||
|
frame: false,
|
||||||
icon: iconPath(),
|
icon: iconPath(),
|
||||||
backgroundColor,
|
backgroundColor: back(mode),
|
||||||
...(process.platform === "darwin" ? { titleBarStyle: "hidden" as const } : {}),
|
|
||||||
...(process.platform === "win32"
|
|
||||||
? {
|
|
||||||
frame: false,
|
|
||||||
titleBarStyle: "hidden" as const,
|
|
||||||
titleBarOverlay: overlay({ mode }),
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(root, "../preload/index.mjs"),
|
preload: join(root, "../preload/index.mjs"),
|
||||||
sandbox: false,
|
sandbox: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
loadWindow(win, "loading.html")
|
win.once("ready-to-show", () => {
|
||||||
|
if (!win.isDestroyed()) win.show()
|
||||||
|
})
|
||||||
|
|
||||||
|
loadSplash(win, mode)
|
||||||
injectGlobals(win, globals)
|
injectGlobals(win, globals)
|
||||||
|
|
||||||
return win
|
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 `<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>OpenCode</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: ${mode};
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: ${bg};
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pulse,
|
||||||
|
#migrate {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pulse[hidden],
|
||||||
|
#migrate[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pulse svg {
|
||||||
|
width: 64px;
|
||||||
|
height: 80px;
|
||||||
|
opacity: 0.5;
|
||||||
|
animation: pulse 1.6s ease-in-out infinite;
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#migrate {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#migrate svg {
|
||||||
|
width: 80px;
|
||||||
|
height: 100px;
|
||||||
|
opacity: 0.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
#copy {
|
||||||
|
display: flex;
|
||||||
|
width: 240px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#status {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
color: ${strong};
|
||||||
|
text-align: center;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bar {
|
||||||
|
width: 80px;
|
||||||
|
height: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: ${track};
|
||||||
|
}
|
||||||
|
|
||||||
|
#fill {
|
||||||
|
width: 25%;
|
||||||
|
height: 100%;
|
||||||
|
background: ${warn};
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root">
|
||||||
|
<div id="pulse">${pulse}</div>
|
||||||
|
<div id="migrate" hidden>
|
||||||
|
${splash}
|
||||||
|
<div id="copy" aria-live="polite">
|
||||||
|
<span id="status">Just a moment...</span>
|
||||||
|
<div id="bar"><div id="fill"></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
;(() => {
|
||||||
|
const lines = ["Just a moment...", "Migrating your database", "This may take a couple of minutes"]
|
||||||
|
const pulse = document.getElementById("pulse")
|
||||||
|
const migrate = document.getElementById("migrate")
|
||||||
|
const status = document.getElementById("status")
|
||||||
|
const fill = document.getElementById("fill")
|
||||||
|
let step = { phase: "server_waiting" }
|
||||||
|
let line = 0
|
||||||
|
let seen = false
|
||||||
|
let value = 0
|
||||||
|
let done = false
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
const sql = step.phase === "sqlite_waiting" || (seen && step.phase === "done")
|
||||||
|
pulse.hidden = sql
|
||||||
|
migrate.hidden = !sql
|
||||||
|
if (!sql) return
|
||||||
|
status.textContent = step.phase === "done" ? "All done" : lines[line]
|
||||||
|
fill.style.width = String(step.phase === "done" ? 100 : Math.max(25, Math.min(100, value))) + "%"
|
||||||
|
}
|
||||||
|
|
||||||
|
function finish() {
|
||||||
|
if (done) return
|
||||||
|
done = true
|
||||||
|
window.api?.loadingWindowComplete?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
function set(step_) {
|
||||||
|
step = step_ || step
|
||||||
|
render()
|
||||||
|
if (step.phase === "done") finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
const timers = [3000, 9000].map((ms, i) =>
|
||||||
|
setTimeout(() => {
|
||||||
|
line = i + 1
|
||||||
|
render()
|
||||||
|
}, ms),
|
||||||
|
)
|
||||||
|
|
||||||
|
const off = window.api?.onInitStep?.((step_) => set(step_)) ?? (() => {})
|
||||||
|
const progress =
|
||||||
|
window.api?.onSqliteMigrationProgress?.((next) => {
|
||||||
|
seen = true
|
||||||
|
if (next.type === "InProgress") {
|
||||||
|
value = Math.max(0, Math.min(100, next.value))
|
||||||
|
step = { phase: "sqlite_waiting" }
|
||||||
|
render()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
value = 100
|
||||||
|
step = { phase: "done" }
|
||||||
|
render()
|
||||||
|
finish()
|
||||||
|
}) ?? (() => {})
|
||||||
|
|
||||||
|
window.api?.awaitInitialization?.((step_) => set(step_))?.catch(() => undefined)
|
||||||
|
|
||||||
|
addEventListener("beforeunload", () => {
|
||||||
|
off()
|
||||||
|
progress()
|
||||||
|
timers.forEach(clearTimeout)
|
||||||
|
})
|
||||||
|
|
||||||
|
render()
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
}
|
||||||
|
|
||||||
|
function mark(base: string, strong: string) {
|
||||||
|
return `<svg viewBox="0 0 80 100" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M60 80H20V40H60V80Z" fill="${base}" /><path d="M60 20H20V80H60V20ZM80 100H0V0H80V100Z" fill="${strong}" /></svg>`
|
||||||
|
}
|
||||||
|
|
||||||
function loadWindow(win: BrowserWindow, html: string) {
|
function loadWindow(win: BrowserWindow, html: string) {
|
||||||
const devUrl = process.env.ELECTRON_RENDERER_URL
|
const devUrl = process.env.ELECTRON_RENDERER_URL
|
||||||
if (devUrl) {
|
if (devUrl) {
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ const api: ElectronAPI = {
|
|||||||
ipcRenderer.removeListener("init-step", handler)
|
ipcRenderer.removeListener("init-step", handler)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
onInitStep: (cb) => {
|
||||||
|
const handler = (_: unknown, step: InitStep) => cb(step)
|
||||||
|
ipcRenderer.on("init-step", handler)
|
||||||
|
return () => ipcRenderer.removeListener("init-step", handler)
|
||||||
|
},
|
||||||
getDefaultServerUrl: () => ipcRenderer.invoke("get-default-server-url"),
|
getDefaultServerUrl: () => ipcRenderer.invoke("get-default-server-url"),
|
||||||
setDefaultServerUrl: (url) => ipcRenderer.invoke("set-default-server-url", url),
|
setDefaultServerUrl: (url) => ipcRenderer.invoke("set-default-server-url", url),
|
||||||
getWslConfig: () => ipcRenderer.invoke("get-wsl-config"),
|
getWslConfig: () => ipcRenderer.invoke("get-wsl-config"),
|
||||||
@@ -60,6 +65,7 @@ const api: ElectronAPI = {
|
|||||||
setZoomFactor: (factor) => ipcRenderer.invoke("set-zoom-factor", factor),
|
setZoomFactor: (factor) => ipcRenderer.invoke("set-zoom-factor", factor),
|
||||||
setTitlebar: (theme) => ipcRenderer.invoke("set-titlebar", theme),
|
setTitlebar: (theme) => ipcRenderer.invoke("set-titlebar", theme),
|
||||||
loadingWindowComplete: () => ipcRenderer.send("loading-window-complete"),
|
loadingWindowComplete: () => ipcRenderer.send("loading-window-complete"),
|
||||||
|
mainWindowReady: () => ipcRenderer.send("main-window-ready"),
|
||||||
runUpdater: (alertOnFail) => ipcRenderer.invoke("run-updater", alertOnFail),
|
runUpdater: (alertOnFail) => ipcRenderer.invoke("run-updater", alertOnFail),
|
||||||
checkUpdate: () => ipcRenderer.invoke("check-update"),
|
checkUpdate: () => ipcRenderer.invoke("check-update"),
|
||||||
installUpdate: () => ipcRenderer.invoke("install-update"),
|
installUpdate: () => ipcRenderer.invoke("install-update"),
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
export type InitStep = { phase: "server_waiting" } | { phase: "sqlite_waiting" } | { phase: "done" }
|
export type InitStep =
|
||||||
|
| { phase: "server_waiting" }
|
||||||
|
| { phase: "sqlite_waiting" }
|
||||||
|
| { phase: "app_waiting" }
|
||||||
|
| { phase: "done" }
|
||||||
|
|
||||||
export type ServerReadyData = {
|
export type ServerReadyData = {
|
||||||
url: string
|
url: string
|
||||||
@@ -19,6 +23,7 @@ export type ElectronAPI = {
|
|||||||
killSidecar: () => Promise<void>
|
killSidecar: () => Promise<void>
|
||||||
installCli: () => Promise<string>
|
installCli: () => Promise<string>
|
||||||
awaitInitialization: (onStep: (step: InitStep) => void) => Promise<ServerReadyData>
|
awaitInitialization: (onStep: (step: InitStep) => void) => Promise<ServerReadyData>
|
||||||
|
onInitStep: (cb: (step: InitStep) => void) => () => void
|
||||||
getDefaultServerUrl: () => Promise<string | null>
|
getDefaultServerUrl: () => Promise<string | null>
|
||||||
setDefaultServerUrl: (url: string | null) => Promise<void>
|
setDefaultServerUrl: (url: string | null) => Promise<void>
|
||||||
getWslConfig: () => Promise<WslConfig>
|
getWslConfig: () => Promise<WslConfig>
|
||||||
@@ -66,6 +71,7 @@ export type ElectronAPI = {
|
|||||||
setZoomFactor: (factor: number) => Promise<void>
|
setZoomFactor: (factor: number) => Promise<void>
|
||||||
setTitlebar: (theme: TitlebarTheme) => Promise<void>
|
setTitlebar: (theme: TitlebarTheme) => Promise<void>
|
||||||
loadingWindowComplete: () => void
|
loadingWindowComplete: () => void
|
||||||
|
mainWindowReady: () => void
|
||||||
runUpdater: (alertOnFail: boolean) => Promise<void>
|
runUpdater: (alertOnFail: boolean) => Promise<void>
|
||||||
checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>
|
checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>
|
||||||
installUpdate: () => Promise<void>
|
installUpdate: () => Promise<void>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const html = async (name: string) => Bun.file(join(dir, name)).text()
|
|||||||
* All local resource references must use relative paths (`./`).
|
* All local resource references must use relative paths (`./`).
|
||||||
*/
|
*/
|
||||||
describe("electron renderer html", () => {
|
describe("electron renderer html", () => {
|
||||||
for (const name of ["index.html", "loading.html"]) {
|
for (const name of ["index.html"]) {
|
||||||
describe(name, () => {
|
describe(name, () => {
|
||||||
test("script src attributes use relative paths", async () => {
|
test("script src attributes use relative paths", async () => {
|
||||||
const content = await html(name)
|
const content = await html(name)
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ render(() => {
|
|||||||
defaultServer={defaultServer.latest ?? ServerConnection.Key.make("sidecar")}
|
defaultServer={defaultServer.latest ?? ServerConnection.Key.make("sidecar")}
|
||||||
servers={servers()}
|
servers={servers()}
|
||||||
router={MemoryRouter}
|
router={MemoryRouter}
|
||||||
|
onReady={() => window.api.mainWindowReady()}
|
||||||
>
|
>
|
||||||
<Inner />
|
<Inner />
|
||||||
</AppInterface>
|
</AppInterface>
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en" style="background-color: var(--background-base)">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<title>OpenCode</title>
|
|
||||||
<link rel="icon" type="image/png" href="./favicon-96x96-v3.png" sizes="96x96" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="./favicon-v3.svg" />
|
|
||||||
<link rel="shortcut icon" href="./favicon-v3.ico" />
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon-v3.png" />
|
|
||||||
<meta name="theme-color" content="#F8F7F7" />
|
|
||||||
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
|
|
||||||
<meta property="og:image" content="./social-share.png" />
|
|
||||||
<meta property="twitter:image" content="./social-share.png" />
|
|
||||||
<script id="oc-theme-preload-script" src="./oc-theme-preload.js"></script>
|
|
||||||
</head>
|
|
||||||
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root" class="flex flex-col h-dvh"></div>
|
|
||||||
<script src="./loading.tsx" type="module"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,83 +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<InitStep | null>(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)
|
|
||||||
|
|
||||||
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(() => {
|
|
||||||
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 (
|
|
||||||
<MetaProvider>
|
|
||||||
<div class="w-screen h-screen bg-background-base flex items-center justify-center">
|
|
||||||
<Font />
|
|
||||||
<div class="flex flex-col items-center gap-11">
|
|
||||||
<Splash class="w-20 h-25 opacity-15" />
|
|
||||||
<div class="w-60 flex flex-col items-center gap-4" aria-live="polite">
|
|
||||||
<span class="w-full overflow-hidden text-center text-ellipsis whitespace-nowrap text-text-strong text-14-normal">
|
|
||||||
{status()}
|
|
||||||
</span>
|
|
||||||
<Progress
|
|
||||||
value={value()}
|
|
||||||
class="w-20 [&_[data-slot='progress-track']]:h-1 [&_[data-slot='progress-track']]:border-0 [&_[data-slot='progress-track']]:rounded-none [&_[data-slot='progress-track']]:bg-surface-weak [&_[data-slot='progress-fill']]:rounded-none [&_[data-slot='progress-fill']]:bg-icon-warning-base"
|
|
||||||
aria-label="Database migration progress"
|
|
||||||
getValueLabel={({ value }) => `${Math.round(value)}%`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</MetaProvider>
|
|
||||||
)
|
|
||||||
}, root)
|
|
||||||
Reference in New Issue
Block a user