Improve CLI preload flow and SSE reconnects

This commit is contained in:
Shantur Rathore
2025-11-20 20:45:31 +00:00
parent 3f46d73a31
commit 30b075e4ba
7 changed files with 147 additions and 107 deletions

View File

@@ -1,20 +1,22 @@
import { app, BrowserView, BrowserWindow, nativeImage, session } from "electron"
import { existsSync } from "fs"
import { dirname, join } from "path"
import { fileURLToPath } from "url"
import { createApplicationMenu } from "./menu"
import { setupCliIPC } from "./ipc"
import { CliProcessManager } from "./process-manager"
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const mainFilename = fileURLToPath(import.meta.url)
const mainDirname = dirname(mainFilename)
const isMac = process.platform === "darwin"
const cliManager = new CliProcessManager()
let mainWindow: BrowserWindow | null = null
let cliView: BrowserView | null = null
let cliViewReady = false
let currentCliUrl: string | null = null
let pendingCliUrl: string | null = null
let loadingScreenVisible = true
let showingLoadingScreen = false
let preloadingView: BrowserView | null = null
if (isMac) {
app.commandLine.appendSwitch("disable-spell-checking")
@@ -25,7 +27,7 @@ function getIconPath() {
return join(process.resourcesPath, "icon.png")
}
return join(__dirname, "../resources/icon.png")
return join(mainDirname, "../resources/icon.png")
}
function getLoadingHtmlPath() {
@@ -33,7 +35,61 @@ function getLoadingHtmlPath() {
return join(process.resourcesPath, "loading.html")
}
return join(__dirname, "../resources/loading.html")
const distResources = join(mainDirname, "../resources/loading.html")
if (existsSync(distResources)) {
return distResources
}
const devResources = join(mainDirname, "../electron/resources/loading.html")
if (existsSync(devResources)) {
return devResources
}
return join(process.cwd(), "electron/resources/loading.html")
}
let cachedPreloadPath: string | null = null
function getPreloadPath() {
if (cachedPreloadPath && existsSync(cachedPreloadPath)) {
return cachedPreloadPath
}
const candidates = [
join(process.resourcesPath, "preload/index.js"),
join(mainDirname, "../preload/index.js"),
join(mainDirname, "../preload/index.cjs"),
join(mainDirname, "../../preload/index.cjs"),
join(mainDirname, "../../electron/preload/index.cjs"),
join(app.getAppPath(), "preload/index.cjs"),
join(app.getAppPath(), "electron/preload/index.cjs"),
]
for (const candidate of candidates) {
if (existsSync(candidate)) {
cachedPreloadPath = candidate
return candidate
}
}
return join(mainDirname, "../preload/index.js")
}
function destroyPreloadingView(target?: BrowserView | null) {
const view = target ?? preloadingView
if (!view) {
return
}
try {
const contents = view.webContents as any
contents?.destroy?.()
} catch (error) {
console.warn("[cli] failed to destroy preloading view", error)
}
if (!target || view === preloadingView) {
preloadingView = null
}
}
function createWindow() {
@@ -49,7 +105,7 @@ function createWindow() {
backgroundColor,
icon: iconPath,
webPreferences: {
preload: join(__dirname, "../preload/index.cjs"),
preload: getPreloadPath(),
contextIsolation: true,
nodeIntegration: false,
spellcheck: !isMac,
@@ -61,8 +117,9 @@ function createWindow() {
}
const loadingHtml = getLoadingHtmlPath()
mainWindow.loadFile(loadingHtml)
loadingScreenVisible = true
showingLoadingScreen = true
currentCliUrl = null
mainWindow.loadFile(loadingHtml).catch((error) => console.error("[cli] failed to load loading screen:", error))
if (process.env.NODE_ENV === "development") {
mainWindow.webContents.openDevTools({ mode: "detach" })
@@ -71,110 +128,96 @@ function createWindow() {
createApplicationMenu(mainWindow)
setupCliIPC(mainWindow, cliManager)
mainWindow.on("resize", resizeCliView)
mainWindow.on("enter-full-screen", resizeCliView)
mainWindow.on("leave-full-screen", resizeCliView)
mainWindow.on("closed", () => {
destroyCliBrowserView()
destroyPreloadingView()
mainWindow = null
currentCliUrl = null
pendingCliUrl = null
showingLoadingScreen = false
})
attachCliBrowserView()
if (pendingCliUrl) {
const url = pendingCliUrl
pendingCliUrl = null
startCliPreload(url)
}
}
function destroyCliBrowserView() {
if (!cliView) {
cliViewReady = false
function showLoadingScreen(force = false) {
if (!mainWindow || mainWindow.isDestroyed()) {
return
}
try {
if (mainWindow && !mainWindow.isDestroyed()) {
try {
mainWindow.removeBrowserView(cliView)
} catch (error) {
console.warn("[cli] failed to remove BrowserView", error)
}
}
const contents = cliView.webContents as any
contents?.destroy?.()
} catch (error) {
console.warn("[cli] failed to destroy BrowserView", error)
if (showingLoadingScreen && !force) {
return
}
cliView = null
cliViewReady = false
destroyPreloadingView()
showingLoadingScreen = true
currentCliUrl = null
pendingCliUrl = null
const loadingHtml = getLoadingHtmlPath()
mainWindow.loadFile(loadingHtml).catch((error) => console.error("[cli] failed to load loading screen:", error))
}
function createCliBrowserView(url: string) {
function startCliPreload(url: string) {
if (!mainWindow || mainWindow.isDestroyed()) {
pendingCliUrl = url
return
}
if (currentCliUrl === url && !showingLoadingScreen) {
return
}
pendingCliUrl = url
cliViewReady = false
destroyCliBrowserView()
destroyPreloadingView()
if (!showingLoadingScreen) {
showLoadingScreen(true)
}
const view = new BrowserView({
webPreferences: {
preload: join(__dirname, "../preload/index.cjs"),
contextIsolation: true,
nodeIntegration: false,
spellcheck: !isMac,
},
})
cliView = view
view.webContents
.loadURL(url)
.catch((error) => console.error("[cli] failed to load BrowserView:", error))
preloadingView = view
view.webContents.once("did-finish-load", () => {
if (cliView !== view) {
const contents = view.webContents as any
contents?.destroy?.()
if (preloadingView !== view) {
destroyPreloadingView(view)
return
}
cliViewReady = true
attachCliBrowserView()
finalizeCliSwap(url)
})
view.webContents.loadURL(url).catch((error) => {
console.error("[cli] failed to preload CLI view:", error)
if (preloadingView === view) {
destroyPreloadingView(view)
}
})
}
function attachCliBrowserView() {
if (!mainWindow || mainWindow.isDestroyed() || !cliView || !cliViewReady) {
return
}
function finalizeCliSwap(url: string) {
destroyPreloadingView()
try {
mainWindow.setBrowserView(cliView)
resizeCliView()
loadingScreenVisible = false
} catch (error) {
console.error("[cli] failed to attach BrowserView:", error)
}
}
function resizeCliView() {
if (!mainWindow || !cliView) {
return
}
const [width, height] = mainWindow.getContentSize()
cliView.setBounds({ x: 0, y: 0, width, height })
cliView.setAutoResize({ width: true, height: true })
}
function showLoadingScreen() {
destroyCliBrowserView()
if (!mainWindow || mainWindow.isDestroyed()) {
pendingCliUrl = url
return
}
if (!loadingScreenVisible) {
const loadingHtml = getLoadingHtmlPath()
loadingScreenVisible = true
mainWindow.loadFile(loadingHtml).catch((error) => console.error("[cli] failed to load loading screen:", error))
}
showingLoadingScreen = false
currentCliUrl = url
pendingCliUrl = null
mainWindow.loadURL(url).catch((error) => console.error("[cli] failed to load CLI view:", error))
}
async function startCli() {
try {
const devMode = process.env.NODE_ENV === "development"
@@ -193,12 +236,11 @@ cliManager.on("ready", (status) => {
if (!status.url) {
return
}
createCliBrowserView(status.url)
startCliPreload(status.url)
})
cliManager.on("status", (status) => {
if (status.state !== "ready") {
pendingCliUrl = null
showLoadingScreen()
}
})

View File

@@ -6,7 +6,8 @@ import { existsSync } from "fs"
import path from "path"
import { buildUserShellCommand, getUserShellEnv, supportsUserShell } from "./user-shell"
const require = createRequire(import.meta.url)
const nodeRequire = createRequire(import.meta.url)
type CliState = "starting" | "ready" | "error" | "stopped"
@@ -283,7 +284,7 @@ export class CliProcessManager extends EventEmitter {
private resolveTsx(): string | null {
try {
const resolved = require.resolve("tsx/dist/cli.js")
const resolved = nodeRequire.resolve("tsx/dist/cli.js")
if (resolved && existsSync(resolved)) {
return resolved
}
@@ -295,8 +296,8 @@ export class CliProcessManager extends EventEmitter {
private tryResolveDist(): string | null {
const candidates: Array<string | (() => string)> = [
() => require.resolve("@codenomad/cli/dist/bin.js"),
() => require.resolve("@codenomad/cli/dist/bin.js", { paths: [app.getAppPath()] }),
() => nodeRequire.resolve("@codenomad/cli/dist/bin.js"),
() => nodeRequire.resolve("@codenomad/cli/dist/bin.js", { paths: [app.getAppPath()] }),
path.join(app.getAppPath(), "node_modules", "@codenomad", "cli", "dist", "bin.js"),
path.resolve(app.getAppPath(), "..", "cli", "dist", "bin.js"),
path.resolve(app.getAppPath(), "..", "packages", "cli", "dist", "bin.js"),